| 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; |
| } |
|
|
| |
| const STATUS_OPTIONS: Record<"known" | "unknown", string[]> = { |
| known: ["home", "lost", "reunited"], |
| unknown: ["pending", "at_shelter", "claimed", "reunited"], |
| }; |
|
|
| |
| |
| 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> |
| ); |
| } |
|
|