PawTrace / frontend /src /components /ComparePhotosModal.tsx
Elliott Duke
Site polish: hero illustration, About rewrite, copy and styling pass
9122959
Raw
History Blame Contribute Delete
3.16 kB
// Side-by-side photo comparison for a match: the visitor's own images on the left, the candidate
// dog's on the right, each column scrolling independently so two photos can be lined up. Shared by
// the home-page tool (uploaded images) and the sample-dog profile (that dog's bundled images).
import type { DogPhoto, PhotoSearchMatch } from "../types";
import { estimatedBreeds } from "./MatchCard";
function pct(score: number): string {
return `${(score * 100).toFixed(1)}%`;
}
/** Wrap plain image URLs (e.g. object URLs from an upload) as the photo shape this modal renders. */
export function photosFromUrls(urls: string[]): DogPhoto[] {
return urls.map((src, i) => ({ id: i, url: src, thumb_url: src, is_primary: i === 0 }));
}
export default function ComparePhotosModal({
match,
queryPhotos,
queryLabel,
isCorrect = false,
onClose,
}: {
match: PhotoSearchMatch;
queryPhotos: DogPhoto[];
queryLabel: string;
/** Sample dogs know their planted twin, so a confirmed match can be badged. */
isCorrect?: boolean;
onClose: () => void;
}) {
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-40"
role="dialog"
onClick={onClose}
>
<div
className="card max-w-4xl w-full max-h-[88vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between mb-3 shrink-0">
<div>
<h3 className="font-bold text-lg flex items-center gap-2">
Compare · <span className="text-brand-600">{pct(match.score)} match</span>
{isCorrect && <span className="badge bg-leaf-600 text-white">✓ Correct match</span>}
</h3>
<p className="text-xs text-gray-500">
{estimatedBreeds(match.dog.predicted_breeds)}
{match.dog.zip ? ` · ZIP ${match.dog.zip}` : ""}
{match.distance_miles != null ? ` · ${match.distance_miles} mi away` : ""}
</p>
</div>
<button className="btn-secondary" onClick={onClose}>
Close
</button>
</div>
<div className="grid grid-cols-2 gap-4 flex-1 min-h-0">
<PhotoColumn label={queryLabel} photos={queryPhotos} />
<PhotoColumn label={`${match.dog.name} (match)`} photos={match.photos} />
</div>
</div>
</div>
);
}
function PhotoColumn({ label, photos }: { label: string; photos: DogPhoto[] }) {
return (
<div className="h-full min-h-0 overflow-y-auto pr-1">
<p className="text-sm font-medium text-gray-700 mb-2 sticky top-0 bg-white py-1 truncate z-10">
{label} <span className="text-gray-400 font-normal">({photos.length})</span>
</p>
<div className="space-y-2">
{photos.map((p) => (
<img
key={p.id}
src={p.url || p.thumb_url || "/paw-placeholder.svg"}
alt={label}
className="w-full h-52 object-contain rounded-md bg-gray-100"
/>
))}
{photos.length === 0 && <p className="text-gray-500 text-sm">No images.</p>}
</div>
</div>
);
}