From 14073c7704031d60d42101df316030476f59ecab Mon Sep 17 00:00:00 2001 From: Taki Koutsomitis Date: Mon, 18 May 2026 19:14:15 -0400 Subject: [PATCH 1/6] Add manual expert --- .../[searchId]/components/AddExpertModal.tsx | 195 ++++++++++++++++++ .../components/SearchDetailContent.tsx | 36 +++- hooks/useExpertFinder.ts | 40 ++++ services/expertFinder.service.ts | 30 +++ types/expertFinder.ts | 2 +- 5 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 app/expert-finder/library/[searchId]/components/AddExpertModal.tsx diff --git a/app/expert-finder/library/[searchId]/components/AddExpertModal.tsx b/app/expert-finder/library/[searchId]/components/AddExpertModal.tsx new file mode 100644 index 000000000..301f2592b --- /dev/null +++ b/app/expert-finder/library/[searchId]/components/AddExpertModal.tsx @@ -0,0 +1,195 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { BaseModal } from '@/components/ui/BaseModal'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/form/Input'; +import { Textarea } from '@/components/ui/form/Textarea'; +import { LoadingButton } from '@/components/ui/LoadingButton'; +import type { AddExpertPayload } from '@/services/expertFinder.service'; +import { extractApiErrorMessage } from '@/services/lib/serviceUtils'; + +export interface AddExpertModalProps { + isOpen: boolean; + onClose: () => void; + onAdd: (searchId: string, payload: AddExpertPayload) => Promise; + searchId: string; + onSuccess: () => Promise; +} + +function buildPayload(values: { + email: string; + honorific: string; + firstName: string; + middleName: string; + lastName: string; + nameSuffix: string; + academicTitle: string; + affiliation: string; + expertise: string; + notes: string; +}): AddExpertPayload { + const t = (s: string) => s.trim(); + const payload: AddExpertPayload = { email: t(values.email).toLowerCase() }; + + if (t(values.honorific)) payload.honorific = t(values.honorific); + if (t(values.firstName)) payload.first_name = t(values.firstName); + if (t(values.middleName)) payload.middle_name = t(values.middleName); + if (t(values.lastName)) payload.last_name = t(values.lastName); + if (t(values.nameSuffix)) payload.name_suffix = t(values.nameSuffix); + if (t(values.academicTitle)) payload.academic_title = t(values.academicTitle); + if (t(values.affiliation)) payload.affiliation = t(values.affiliation); + if (t(values.expertise)) payload.expertise = t(values.expertise); + if (t(values.notes)) payload.notes = t(values.notes); + + return payload; +} + +export function AddExpertModal({ + isOpen, + onClose, + onAdd, + searchId, + onSuccess, +}: AddExpertModalProps) { + const [email, setEmail] = useState(''); + const [honorific, setHonorific] = useState(''); + const [firstName, setFirstName] = useState(''); + const [middleName, setMiddleName] = useState(''); + const [lastName, setLastName] = useState(''); + const [nameSuffix, setNameSuffix] = useState(''); + const [academicTitle, setAcademicTitle] = useState(''); + const [affiliation, setAffiliation] = useState(''); + const [expertise, setExpertise] = useState(''); + const [notes, setNotes] = useState(''); + const [submitError, setSubmitError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!isOpen) return; + setEmail(''); + setHonorific(''); + setFirstName(''); + setMiddleName(''); + setLastName(''); + setNameSuffix(''); + setAcademicTitle(''); + setAffiliation(''); + setExpertise(''); + setNotes(''); + setSubmitError(null); + }, [isOpen]); + + const handleSubmit = async () => { + const trimmedEmail = email.trim(); + if (!trimmedEmail) { + setSubmitError('Email is required.'); + return; + } + + setSubmitError(null); + setIsSubmitting(true); + try { + const payload = buildPayload({ + email, + honorific, + firstName, + middleName, + lastName, + nameSuffix, + academicTitle, + affiliation, + expertise, + notes, + }); + await onAdd(searchId, payload); + await onSuccess(); + onClose(); + } catch (err) { + setSubmitError(extractApiErrorMessage(err, 'Failed to add expert')); + } finally { + setIsSubmitting(false); + } + }; + + return ( + +
+ setEmail(e.target.value)} + placeholder="jane.doe@university.edu" + /> +
+ setHonorific(e.target.value)} + placeholder="e.g. Dr, Prof, Mr, Ms" + /> + setFirstName(e.target.value)} + /> + setMiddleName(e.target.value)} + /> + setLastName(e.target.value)} /> + setNameSuffix(e.target.value)} + placeholder="e.g. Jr., Sr., III, IV" + /> + setAcademicTitle(e.target.value)} + placeholder="e.g. Associate Professor" + /> +
+ setAffiliation(e.target.value)} + placeholder="e.g. MIT" + /> + setExpertise(e.target.value)} + placeholder="e.g. Quantum computing" + /> +