From 6af4fc1608fe64010cd234092cc7b6ffcf631591 Mon Sep 17 00:00:00 2001 From: Tanuja Pansare Date: Tue, 4 Aug 2026 17:05:02 +0530 Subject: [PATCH] Add event photo search --- .../components/dashboard/EventHomeModules.tsx | 38 +- .../panels/EventPhotoSearchPanel.tsx | 590 ++++++++++++++++++ src/services/api/eventPhoto.ts | 58 ++ src/services/api/index.ts | 1 + 4 files changed, 683 insertions(+), 4 deletions(-) create mode 100644 src/app/components/panels/EventPhotoSearchPanel.tsx create mode 100644 src/services/api/eventPhoto.ts diff --git a/src/app/components/dashboard/EventHomeModules.tsx b/src/app/components/dashboard/EventHomeModules.tsx index c0886b2d..48de1773 100644 --- a/src/app/components/dashboard/EventHomeModules.tsx +++ b/src/app/components/dashboard/EventHomeModules.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { BarChart2, CalendarDays, Check, FileText, Globe, Link2, Mail, Plus, Share2, ShieldCheck, Tag, Ticket, Trophy, Upload } from "lucide-react"; +import { BarChart2, CalendarDays, Check, FileText, Globe, Image, Link2, Mail, Plus, Share2, ShieldCheck, Tag, Ticket, Trophy, Upload } from "lucide-react"; import { useBreakpoint } from "../../hooks/useBreakpoint"; import { useAppNav } from "../../contexts/AppNavContext"; import { useAuth } from "../../contexts/AuthContext"; @@ -19,6 +19,7 @@ import { ShareFormPanel } from "../forms/ShareFormPanel"; import { CreateBadgePanel } from "../panels/CreateBadgePanel"; import { EventBadgesPanel } from "./EventBadgesPanel"; import { EventDocumentsPanel } from "./EventDocumentsPanel"; +import { EventPhotoSearchPanel } from "../panels/EventPhotoSearchPanel"; import { EventEmailCampaignsPanel } from "./EventEmailCampaignsPanel"; import { AddQuizPollModal, buildQuizPollSettingsPayload } from "../quiz/AddQuizPollModal"; import type { QuizPollModalForm } from "../quiz/AddQuizPollModal"; @@ -262,7 +263,8 @@ export function EventHomeModules() { const [eventSettings, setEventSettings] = useState>({}); const [webCheckInEnabled, setWebCheckInEnabled] = useState(false); const [webCheckInLoading, setWebCheckInLoading] = useState(false); - const [showDocumentsPanel, setShowDocumentsPanel] = useState(false); +const [showDocumentsPanel, setShowDocumentsPanel] = useState(false); + const [showEventPhotoSearchPanel, setShowEventPhotoSearchPanel] = useState(false); const [emailCampaignCount, setEmailCampaignCount] = useState(0); const [emailCampaignLoading, setEmailCampaignLoading] = useState(false); const [showCreateEmailCampaign, setShowCreateEmailCampaign] = useState(false); @@ -1097,6 +1099,25 @@ export function EventHomeModules() { } /> ) : null} + } + title="Event Photo Search" + description="Upload event photos and search by image" + actions={ + + } + /> {canReadEmailCampaigns ? ( } @@ -1235,7 +1256,7 @@ export function EventHomeModules() { /> ) : null} - {showDocumentsPanel && selectedEventId ? ( +{showDocumentsPanel && selectedEventId ? ( ) : null} - {showEmailCampaignsPanel && selectedEventId ? ( + {showEventPhotoSearchPanel && selectedEventId ? ( + setShowEventPhotoSearchPanel(false)} + /> + ) : null} + +{showEmailCampaignsPanel && selectedEventId ? ( void; +}; + +function formatWhen(iso?: string) { + if (!iso) return '—'; + try { + return new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }); + } catch { + return '—'; + } +} + +export function EventPhotoSearchPanel({ open, eventId, eventName, onClose }: Props) { + const [loadingPhotos, setLoadingPhotos] = useState(false); + const [photos, setPhotos] = useState([]); + const [err, setErr] = useState(null); + + + const [files, setFiles] = useState([]); + const [uploading, setUploading] = useState(false); + const fileInputRef = useRef(null); + + const [queryFile, setQueryFile] = useState(null); + const queryFileInputRef = useRef(null); + const [searchResults, setSearchResults] = useState(null); + const [searching, setSearching] = useState(false); + + const [deletedMsg, setDeletedMsg] = useState(null); + + const loadPhotos = async () => { + setLoadingPhotos(true); + setErr(null); + try { + const r = await eventPhotoApi.listEventPhotos(eventId); + setPhotos(r.data?.photos ?? []); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Failed to load photos'); + setPhotos([]); + } finally { + setLoadingPhotos(false); + } + }; + + useEffect(() => { + if (!open) return; + void loadPhotos(); + setSearchResults(null); + setQueryFile(null); + setFiles([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, eventId]); + + const onUpload = async () => { + if (files.length === 0) return; + setUploading(true); + setErr(null); + try { + const res = await eventPhotoApi.uploadEventPhoto(eventId, files); + if (!res.ok) { + let msg = res.statusText; + try { + const j = await res.json(); + msg = j?.error?.message || msg; + } catch {} + throw new Error(msg); + } + setFiles([]); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + await loadPhotos(); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Upload failed'); + } finally { + setUploading(false); + } + }; + + const onDelete = async (photo: EventPhotoRow) => { + const ok = window.confirm(`Delete "${photo.originalFileName}"?`); + if (!ok) return; + + setErr(null); + try { + const res = await eventPhotoApi.deleteEventPhoto(eventId, photo._id); + if (!res.ok) { + let msg = res.statusText; + try { + const j = await res.json(); + msg = j?.error?.message || msg; + } catch {} + throw new Error(msg); + } + setDeletedMsg('Photo deleted.'); + await loadPhotos(); + setTimeout(() => setDeletedMsg(null), 1800); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Delete failed'); + } + }; + + const onFindPhotos = async () => { + if (!queryFile) return; + setSearching(true); + setErr(null); + // For now, just show a placeholder — backend search integration to be added later. + // This simulates a search by showing all photos (no actual vector search yet). + try { + const r = await eventPhotoApi.searchEventPhotos( + eventId, + queryFile + ); + + if (!r.ok) { + throw new Error("Search failed"); + } + + const data = await r.json(); + +console.log(data); + +setSearchResults(data.data.matches ?? []); + +// setSearchResults(data.matches ?? []); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Search failed'); + setSearchResults(null); + } finally { + setSearching(false); + } + }; + + if (!open) return null; + + const displayPhotos = searchResults !== null ? searchResults : photos; + + return ( + <> +
+ +
+ + + {/* Header */} +
+
+

+ Event Photo Search +

+

+ {eventName || 'Photos'} +

+

+ Upload event photos +

+
+ + +
+ + {/* Upload Section */} +
+
+
+ + {/* setDocId(e.target.value)} + style={{ + width: '100%', + padding: '8px 10px', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-button)', + background: 'var(--card)', + color: 'var(--foreground)', + fontSize: 'var(--text-sm)', + fontFamily: 'var(--font-noto-sans)', + outline: 'none', + marginBottom: 8, + boxSizing: 'border-box', + }} + /> */} +
+ + + {files.length === 0 ? ( + "No files selected" + ) : ( + files.map(f => f.name).join(", ") + )} + +
+ { + const selected = Array.from(e.target.files || []); + setFiles(selected); + }} + /> +

+ Supported: JPEG, PNG, GIF, WEBP, BMP +

+
+ +
+
+ + {/* Find Photos Section */} +
+
+
+ + + Find Photos + +
+ { + const f = e.target.files?.[0] || null; + setQueryFile(f); + if (f) { + // Auto-trigger search when file is selected + setSearchResults(null); + } + }} + /> + {!queryFile ? ( + + ) : ( +
+ + {queryFile.name} + + + +
+ )} +
+ {searchResults !== null && ( +

+ Showing {searchResults.length} result{searchResults.length === 1 ? '' : 's'} + +

+ )} +
+ + {/* Photo List */} +
+ {err ? ( +

{err}

+ ) : null} + {deletedMsg ? ( +

{deletedMsg}

+ ) : null} + + {loadingPhotos ? ( +

Loading…

+ ) : displayPhotos.length === 0 ? ( +
+
+ +

+ No photos +

+

+ Upload the first photo for this event. +

+
+
+ ) : ( +
+
+ {displayPhotos.map((p) => ( +
+
+ {p.url ? ( + {p.originalFileName} + ) : ( + + )} +
+
+

+ {p.originalFileName} +

+

+ Document ID: {p._id} +

+

+ {formatWhen(p.uploadedAt)} +

+ {p.embedding_generated && ( + + Embedding Ready + + )} +
+ + +
+
+
+ ))} +
+
+ )} +
+
+ + ); +} + diff --git a/src/services/api/eventPhoto.ts b/src/services/api/eventPhoto.ts new file mode 100644 index 00000000..d0ab5d50 --- /dev/null +++ b/src/services/api/eventPhoto.ts @@ -0,0 +1,58 @@ +import { api, fetchWithAuth, API_BASE_URL } from './client'; + +export interface EventPhotoRow { + _id: string; + event: string; + organization: string; + uploadedBy?: string | null; + + fileName: string; + originalFileName: string; + + url: string; + s3Key: string; + + mimeType?: string; + size?: number; + + embedding_generated: boolean; + uploadedAt?: string; +} + +export const eventPhotoApi = { + listEventPhotos(eventId: string) { + const path = `/events/${eventId}/photos`; + return api.get<{ photos: EventPhotoRow[] }>(path); + }, + + uploadEventPhoto(eventId: string, files: File[]) { + const fd = new FormData(); + + files.forEach(file => { + fd.append("files", file, file.name); + }); + + return fetchWithAuth(`/events/${eventId}/photos`, { + method: "POST", + body: fd, + }); +}, + +searchEventPhotos(eventId: string, file: File) { + const fd = new FormData(); + + fd.append("files", file); + + return fetchWithAuth(`/events/${eventId}/photos/search`, { + method: "POST", + body: fd, + }); +}, + + async deleteEventPhoto(eventId: string, photoId: string) { + return fetchWithAuth(`/events/${eventId}/photos/${photoId}`, { + method: 'DELETE', + }); + }, +}; + diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 50597c62..20944549 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -12,6 +12,7 @@ export { eventsApi } from './events'; export { rewardPointsApi, type LeaderboardRow, type CreateRewardPointPayload } from './rewardPoints'; export { participantsApi, type ParticipantWithEvent } from './participants'; export { participantDocumentsApi, type ParticipantDocumentRow } from './participantDocuments'; +export { eventPhotoApi, type EventPhotoRow } from './eventPhoto'; export { attendancesApi,