PawTrace / frontend /src /components /DogPhotosModal.tsx
Elliott Duke
Site polish: hero illustration, About rewrite, copy and styling pass
9122959
Raw
History Blame Contribute Delete
4.31 kB
import { useCallback, useEffect, useState } from "react";
import { api } from "../api";
import type { DogDetailResult } from "../types";
import { estimatedBreeds } from "./MatchCard";
interface Props {
kind: "known" | "unknown";
id: number;
onClose: () => void;
onChanged?: () => void; // called after an admin status change so parent lists can refresh
}
// Valid statuses per dog kind (admin can set these — e.g. a found dog "at_shelter" vs "reunited").
const STATUS_OPTIONS: Record<"known" | "unknown", string[]> = {
known: ["home", "lost", "reunited"],
unknown: ["pending", "at_shelter", "claimed", "reunited"],
};
// Modal showing a dog's profile + ALL of its photos (full images). Admin-only, so it also lets the
// admin change the dog's status.
export default function DogPhotosModal({ kind, id, onClose, onChanged }: Props) {
const [data, setData] = useState<DogDetailResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [savingStatus, setSavingStatus] = useState(false);
const load = useCallback(() => {
setError(null);
return api
.getDogDetail(kind, id)
.then(setData)
.catch((e) => setError(e instanceof Error ? e.message : "Failed to load"));
}, [kind, id]);
useEffect(() => {
setData(null);
load();
}, [load]);
async function changeStatus(status: string) {
setSavingStatus(true);
setError(null);
try {
await api.setDogStatus(kind, id, status);
await load();
onChanged?.();
} catch (e) {
setError(e instanceof Error ? e.message : "Could not update status");
} finally {
setSavingStatus(false);
}
}
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-30"
role="dialog"
onClick={onClose}
>
<div
className="card max-w-3xl w-full max-h-[85vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
{!data && !error ? (
<p className="text-gray-500">Loading…</p>
) : data ? (
<>
<div className="flex items-start justify-between mb-1 gap-3">
<div>
<h3 className="font-bold text-lg">{data.profile.name}</h3>
<p className="text-xs text-gray-500">
{data.profile.kind} #{data.profile.id}
{" · "}
{estimatedBreeds(data.profile.predicted_breeds)}
{data.profile.zip ? ` · ${data.profile.zip}` : ""}
{data.profile.dataset_name ? ` · ${data.profile.dataset_name}` : ""}
</p>
</div>
<button className="btn-secondary" onClick={onClose}>
Close
</button>
</div>
<div className="flex items-center gap-2 my-3">
<label className="label mb-0" htmlFor="dog-status">
Status
</label>
<select
id="dog-status"
className="input w-auto"
value={data.profile.status}
disabled={savingStatus}
onChange={(e) => changeStatus(e.target.value)}
>
{STATUS_OPTIONS[kind].map((s) => (
<option key={s} value={s}>
{s.replace("_", " ")}
</option>
))}
</select>
{savingStatus && <span className="text-xs text-gray-500">Saving…</span>}
</div>
<p className="text-xs text-gray-400 mb-3">{data.photos.length} photo(s)</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{data.photos.map((p) => (
<img
key={p.id}
src={p.url || p.thumb_url || "/paw-placeholder.svg"}
alt={data.profile.name}
className="w-full h-48 object-contain rounded-md bg-gray-100"
/>
))}
{data.photos.length === 0 && (
<p className="text-gray-500 text-sm col-span-full">No photos.</p>
)}
</div>
</>
) : null}
</div>
</div>
);
}