571 lines
15 KiB
TypeScript
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>
|
|
);
|
|
} |