'use client'; import { useState, useEffect, useRef } from 'react'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { LoginDialog } from '@/components/LoginDialog'; import { RegisterDialog } from '@/components/RegisterDialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Search } from 'lucide-react'; import { cn } from '@/lib/utils'; interface IdentifyFaceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; faceId: number; existingPerson?: { firstName: string; lastName: string; middleName?: string | null; maidenName?: string | null; dateOfBirth?: Date | null; } | null; onSave: (data: { personId?: number; firstName?: string; lastName?: string; middleName?: string; maidenName?: string; dateOfBirth?: Date; }) => Promise; } export function IdentifyFaceDialog({ open, onOpenChange, faceId, existingPerson, onSave, }: IdentifyFaceDialogProps) { const { data: session, status, update } = useSession(); const router = useRouter(); const [firstName, setFirstName] = useState(existingPerson?.firstName || ''); const [lastName, setLastName] = useState(existingPerson?.lastName || ''); const [middleName, setMiddleName] = useState(existingPerson?.middleName || ''); const [maidenName, setMaidenName] = useState(existingPerson?.maidenName || ''); const [isSaving, setIsSaving] = useState(false); const [errors, setErrors] = useState<{ firstName?: string; lastName?: string; }>({}); const isAuthenticated = status === 'authenticated'; const hasWriteAccess = session?.user?.hasWriteAccess === true; const isLoading = status === 'loading'; const [mounted, setMounted] = useState(false); const [loginDialogOpen, setLoginDialogOpen] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); const [showRegisteredMessage, setShowRegisteredMessage] = useState(false); const [mode, setMode] = useState<'existing' | 'new'>('existing'); const [people, setPeople] = useState>([]); const [selectedPersonId, setSelectedPersonId] = useState(null); const [peopleSearchQuery, setPeopleSearchQuery] = useState(''); const [peoplePopoverOpen, setPeoplePopoverOpen] = useState(false); const [loadingPeople, setLoadingPeople] = useState(false); // Dragging state const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const dialogRef = useRef(null); // Prevent hydration mismatch by only rendering on client useEffect(() => { setMounted(true); }, []); // Reset position when dialog opens useEffect(() => { if (open) { setPosition({ x: 0, y: 0 }); // Reset mode and selected person when dialog opens setMode('existing'); setSelectedPersonId(null); setPeopleSearchQuery(''); } }, [open]); // Fetch people when dialog opens useEffect(() => { if (open && mode === 'existing' && people.length === 0) { fetchPeople(); } }, [open, mode]); const fetchPeople = async () => { setLoadingPeople(true); try { const response = await fetch('/api/people'); if (!response.ok) throw new Error('Failed to fetch people'); const data = await response.json(); setPeople(data.people); } catch (error) { console.error('Error fetching people:', error); } finally { setLoadingPeople(false); } }; // Handle drag start const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); // Prevent text selection and other default behaviors if (dialogRef.current) { setIsDragging(true); const rect = dialogRef.current.getBoundingClientRect(); // Calculate the center of the dialog const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; // Store the offset from mouse to dialog center setDragStart({ x: e.clientX - centerX, y: e.clientY - centerY, }); } }; // Handle dragging useEffect(() => { if (!isDragging) return; const handleMouseMove = (e: MouseEvent) => { // Calculate new position relative to center (50%, 50%) const newX = e.clientX - window.innerWidth / 2 - dragStart.x; const newY = e.clientY - window.innerHeight / 2 - dragStart.y; setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { setIsDragging(false); }; window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, dragStart]); const handleSave = async () => { // Reset errors setErrors({}); if (mode === 'existing') { // Validate person selection if (!selectedPersonId) { alert('Please select a person'); return; } setIsSaving(true); try { await onSave({ personId: selectedPersonId }); // Show success message alert('Identification submitted successfully! It will be reviewed by an administrator before being applied.'); onOpenChange(false); } catch (error: any) { console.error('Error saving face identification:', error); alert(error.message || 'Failed to submit identification. Please try again.'); } finally { setIsSaving(false); } } else { // Validate required fields for new person const newErrors: typeof errors = {}; if (!firstName.trim()) { newErrors.firstName = 'First name is required'; } if (!lastName.trim()) { newErrors.lastName = 'Last name is required'; } if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } setIsSaving(true); try { await onSave({ firstName: firstName.trim(), lastName: lastName.trim(), middleName: middleName.trim() || undefined, maidenName: maidenName.trim() || undefined, }); // Show success message alert('Identification submitted successfully! It will be reviewed by an administrator before being applied.'); onOpenChange(false); // Reset form after successful save if (!existingPerson) { setFirstName(''); setLastName(''); setMiddleName(''); setMaidenName(''); } } catch (error: any) { console.error('Error saving face identification:', error); setErrors({ ...errors, // Show error message }); alert(error.message || 'Failed to submit identification. Please try again.'); } finally { setIsSaving(false); } } }; // Prevent hydration mismatch - don't render until mounted if (!mounted) { return null; } // Handle successful login/register - refresh session const handleAuthSuccess = async () => { await update(); router.refresh(); }; // Show login prompt if not authenticated if (!isLoading && !isAuthenticated) { return ( <> Sign In Required You need to be signed in to identify faces. Your identifications will be submitted for approval.

Please sign in or create an account to continue.

{ setLoginDialogOpen(open); if (!open) { setShowRegisteredMessage(false); } }} onSuccess={handleAuthSuccess} onOpenRegister={() => { setLoginDialogOpen(false); setRegisterDialogOpen(true); }} registered={showRegisteredMessage} callbackUrl={typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/'} /> { setRegisterDialogOpen(open); if (!open) { setShowRegisteredMessage(false); } }} onSuccess={handleAuthSuccess} onOpenLogin={() => { setShowRegisteredMessage(true); setRegisterDialogOpen(false); setLoginDialogOpen(true); }} callbackUrl={typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/'} /> ); } // Show write access required message if authenticated but no write access if (!isLoading && isAuthenticated && !hasWriteAccess) { return ( Write Access Required You need write access to identify faces.

Only users with write access can identify faces. Please contact an administrator to request write access.

); } return ( Identify Face Choose an existing person or add a new person to identify this face. Your identification will be submitted for approval. {isLoading ? (
Loading...
) : (
{/* Mode selector */}
{mode === 'existing' ? (
{ event.stopPropagation(); }} >
setPeopleSearchQuery(e.target.value)} className="mb-2" />
event.stopPropagation()} > {people.filter((person) => { const fullName = `${person.firstName} ${person.lastName} ${person.middleName || ''} ${person.maidenName || ''}`.toLowerCase(); return fullName.includes(peopleSearchQuery.toLowerCase()); }).length === 0 ? (

No people found

) : (
{people .filter((person) => { const fullName = `${person.firstName} ${person.lastName} ${person.middleName || ''} ${person.maidenName || ''}`.toLowerCase(); return fullName.includes(peopleSearchQuery.toLowerCase()); }) .map((person) => { const isSelected = selectedPersonId === person.id; return (
{ setSelectedPersonId(person.id); setPeoplePopoverOpen(false); }} >
{person.firstName} {person.lastName}
{(person.middleName || person.maidenName) && (
{[person.middleName, person.maidenName].filter(Boolean).join(' • ')}
)}
); })}
)}
) : ( <>
setFirstName(e.target.value)} placeholder="Enter first name" className={cn(errors.firstName && 'border-red-500')} /> {errors.firstName && (

{errors.firstName}

)}
setLastName(e.target.value)} placeholder="Enter last name" className={cn(errors.lastName && 'border-red-500')} /> {errors.lastName && (

{errors.lastName}

)}
setMiddleName(e.target.value)} placeholder="Enter middle name (optional)" />
setMaidenName(e.target.value)} placeholder="Enter maiden name (optional)" />
)}
)}
); }