Eventbuzz_Web/src/pages/EventPhotoSearchPage.tsx

571 lines
15 KiB
TypeScript

import React, { useState } from "react";
import { Search, Upload, X } from "lucide-react";
import { useParams } from "react-router-dom";
import { eventPhotoApi } from "../api/eventPhoto";
export function EventPhotoSearchPage() {
const { eventId = "" } = useParams<{ eventId: string }>();
const [queryFile, setQueryFile] = useState<File | null>(null);
const [searching, setSearching] = useState(false);
const [results, setResults] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
if (!file) return;
setQueryFile(file);
setResults([]);
setError(null);
};
const clearFile = () => {
setQueryFile(null);
setResults([]);
setError(null);
};
const handleSearch = async () => {
if (!eventId) {
setError("Missing event id.");
return;
}
if (!queryFile) {
setError("Please select a photo.");
return;
}
setSearching(true);
setError(null);
try {
const response =
await eventPhotoApi.searchPublicEventPhotos(
eventId,
queryFile
);
if (!response.ok) {
let message = response.statusText || "Search failed";
try {
const body = await response.json();
message = body?.error?.message || body?.message || message;
} catch {
// Ignore JSON parse errors and use the default HTTP error
}
throw new Error(message);
}
const body = await response.json();
const matches = Array.isArray(body?.data?.matches) ? body.data.matches : [];
setResults(matches);
if (matches.length === 0 && body?.message) {
setError(body.message);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Photo search failed."
);
setResults([]);
} finally {
setSearching(false);
}
};
return (
<div
style={{
minHeight: "100vh",
background: "#f8fafc",
padding: "32px 24px",
fontFamily: "var(--font-noto-sans)",
}}
>
<div
style={{
maxWidth: "1100px",
margin: "0 auto",
}}
>
{/* Page Header */}
<div style={{ marginBottom: "28px" }}>
<div
style={{
fontSize: "13px",
color: "#64748b",
marginBottom: "6px",
}}
>
Event Photo Search
</div>
<h1
style={{
margin: 0,
fontSize: "28px",
lineHeight: 1.2,
fontWeight: 600,
color: "#0f172a",
fontFamily: "var(--font-inter)",
}}
>
Search Event Photos
</h1>
<p
style={{
margin: "8px 0 0",
fontSize: "14px",
color: "#64748b",
}}
>
Upload a photo to find matching photos from this event.
</p>
</div>
{/* Search Card */}
<div
style={{
background: "#ffffff",
border: "1px solid #e2e8f0",
borderRadius: "12px",
padding: "24px",
boxShadow: "0 1px 3px rgba(15, 23, 42, 0.06)",
}}
>
{/* Card Header */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
marginBottom: "20px",
}}
>
<div
style={{
width: "42px",
height: "42px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "10px",
background: "#eef2ff",
color: "#f59e0b",
}}
>
<Search size={21} />
</div>
<div>
<div
style={{
fontSize: "17px",
fontWeight: 600,
color: "#0f172a",
}}
>
Find Photos
</div>
<div
style={{
marginTop: "2px",
fontSize: "13px",
color: "#64748b",
}}
>
Select one photo to search for matching event photos.
</div>
</div>
</div>
<input
id="query-photo"
type="file"
accept="image/jpeg,image/png,image/jpg"
onChange={handleFileChange}
style={{ display: "none" }}
/>
{!queryFile ? (
/* Upload Area */
<label
htmlFor="query-photo"
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "220px",
padding: "30px",
border: "1.5px dashed #cbd5e1",
borderRadius: "10px",
background: "#f8fafc",
cursor: "pointer",
transition: "all 0.2s ease",
textAlign: "center",
}}
>
<div
style={{
width: "54px",
height: "54px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
background: "#fff7ed",
color: "#f59e0b",
marginBottom: "14px",
}}
>
<Upload size={24} />
</div>
<div
style={{
fontSize: "15px",
fontWeight: 600,
color: "#0f172a",
}}
>
Select a photo
</div>
<div
style={{
marginTop: "6px",
fontSize: "13px",
color: "#64748b",
}}
>
Choose one JPG or PNG image from your device
</div>
<div
style={{
marginTop: "16px",
padding: "8px 16px",
border: "1px solid #f59e0b",
borderRadius: "7px",
background: "#ffffff",
color: "#d97706",
fontSize: "13px",
fontWeight: 600,
}}
>
Browse Photo
</div>
</label>
) : (
/* Selected Photo */
<div
style={{
border: "1px solid #e2e8f0",
borderRadius: "10px",
padding: "16px",
background: "#f8fafc",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "16px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
minWidth: 0,
}}
>
<div
style={{
width: "44px",
height: "44px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "8px",
background: "#fff7ed",
color: "#f59e0b",
flexShrink: 0,
}}
>
<Upload size={19} />
</div>
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: "14px",
fontWeight: 600,
color: "#0f172a",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{queryFile.name}
</div>
<div
style={{
marginTop: "3px",
fontSize: "12px",
color: "#64748b",
}}
>
Query photo selected
</div>
</div>
</div>
<button
type="button"
onClick={clearFile}
title="Remove selected photo"
aria-label="Remove selected photo"
style={{
width: "34px",
height: "34px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
border: "1px solid #e2e8f0",
borderRadius: "7px",
background: "#ffffff",
color: "#64748b",
cursor: "pointer",
}}
>
<X size={16} />
</button>
</div>
{/* Search Button */}
<button
type="button"
onClick={handleSearch}
disabled={searching}
style={{
width: "100%",
marginTop: "16px",
height: "44px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
border: "none",
borderRadius: "7px",
background: searching ? "#94a3b8" : "#2563eb",
color: "#ffffff",
cursor: searching ? "not-allowed" : "pointer",
fontSize: "14px",
fontWeight: 600,
}}
>
<Search size={17} />
{searching ? "Searching Photos..." : "Search Photos"}
</button>
</div>
)}
{error && (
<div
style={{
marginTop: "14px",
padding: "11px 14px",
borderRadius: "7px",
border: "1px solid #fecaca",
background: "#fef2f2",
color: "#dc2626",
fontSize: "13px",
}}
>
{error}
</div>
)}
</div>
{/* Loading */}
{searching && (
<div
style={{
marginTop: "24px",
padding: "24px",
background: "#ffffff",
border: "1px solid #e2e8f0",
borderRadius: "12px",
textAlign: "center",
color: "#64748b",
fontSize: "14px",
}}
>
Searching matching photos...
</div>
)}
{/* Results */}
{results.length > 0 && (
<div style={{ marginTop: "28px" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "14px",
}}
>
<div>
<h2
style={{
margin: 0,
fontSize: "18px",
fontWeight: 600,
color: "#0f172a",
}}
>
Matching Photos
</h2>
<p
style={{
margin: "4px 0 0",
fontSize: "13px",
color: "#64748b",
}}
>
{results.length} matching photo
{results.length !== 1 ? "s" : ""} found
</p>
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fill, minmax(220px, 1fr))",
gap: "16px",
}}
>
{results.map((photo) => (
<div
key={photo._id}
style={{
background: "#ffffff",
border: "1px solid #e2e8f0",
borderRadius: "10px",
overflow: "hidden",
boxShadow:
"0 1px 3px rgba(15, 23, 42, 0.05)",
}}
>
{photo.url && (
<img
src={photo.url}
alt={
photo.originalFileName || "Event photo"
}
style={{
width: "100%",
height: "200px",
display: "block",
objectFit: "cover",
}}
/>
)}
<div style={{ padding: "12px" }}>
<div
style={{
fontSize: "13px",
color: "#0f172a",
fontWeight: 600,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{photo.originalFileName || "Event photo"}
</div>
{typeof photo.similarity === "number" && (
<div
style={{
marginTop: "6px",
fontSize: "12px",
color: "#64748b",
}}
>
Match:{" "}
{(
Math.round(photo.similarity * 10) / 10
).toFixed(1)}
%
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* No Results */}
{!searching &&
queryFile &&
results.length === 0 &&
!error && (
<div
style={{
marginTop: "24px",
padding: "30px",
background: "#ffffff",
border: "1px solid #e2e8f0",
borderRadius: "12px",
textAlign: "center",
color: "#64748b",
}}
>
<Search
size={28}
style={{ marginBottom: "8px" }}
/>
<div
style={{
fontSize: "14px",
fontWeight: 600,
color: "#334155",
}}
>
No matching photos
</div>
<div
style={{
marginTop: "5px",
fontSize: "13px",
}}
>
Search for another photo from this event.
</div>
</div>
)}
</div>
</div>
);
}