TripDetailsPage.tsx
4,249 bytes
| 1 | import { useState, type CSSProperties } from "react"; |
|---|---|
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 3 | import { ArrowLeft, CalendarDays, Check, Edit3, MapPin, Plus, Trash2 } from "lucide-react"; |
| 4 | import { Link, useNavigate, useParams } from "react-router-dom"; |
| 5 | import { ProgressRing } from "../components/ProgressRing"; |
| 6 | import { useQuickTaste } from "../components/QuickTaste"; |
| 7 | import { TripFormDialog } from "../components/TripFormDialog"; |
| 8 | import { ErrorState, LoadingScreen, StatusPill } from "../components/Ui"; |
| 9 | import { api } from "../lib/api"; |
| 10 | import { formatDateRange } from "../lib/format"; |
| 11 | import type { Trip } from "../types"; |
| 12 | |
| 13 | export function TripDetailsPage() { |
| 14 | const { tripId = "" } = useParams(); |
| 15 | const navigate = useNavigate(); |
| 16 | const queryClient = useQueryClient(); |
| 17 | const { openTaste } = useQuickTaste(); |
| 18 | const [editing, setEditing] = useState(false); |
| 19 | const trip = useQuery({ |
| 20 | queryKey: ["trip", tripId], |
| 21 | queryFn: () => api<Trip>(`/api/v1/trips/${tripId}`), |
| 22 | enabled: Boolean(tripId) |
| 23 | }); |
| 24 | const remove = useMutation({ |
| 25 | mutationFn: () => api<void>(`/api/v1/trips/${tripId}`, { method: "DELETE" }), |
| 26 | onSuccess: async () => { |
| 27 | await queryClient.invalidateQueries(); |
| 28 | navigate("/app/trips"); |
| 29 | } |
| 30 | }); |
| 31 | |
| 32 | if (trip.isLoading) return <LoadingScreen label="Opening your food mission" />; |
| 33 | if (trip.isError || !trip.data) return <ErrorState message="This trip could not be loaded." />; |
| 34 | const data = trip.data; |
| 35 | |
| 36 | return ( |
| 37 | <div className="page trip-detail-page"> |
| 38 | <Link className="back-link" to="/app/trips"><ArrowLeft size={16} /> Trips</Link> |
| 39 | <section className="trip-detail-hero" style={{ "--destination-accent": data.destination.accentColor } as CSSProperties}> |
| 40 | <div> |
| 41 | <div className="trip-detail-labels"><StatusPill status={data.status} /><span>{data.destination.code}</span></div> |
| 42 | <h1>{data.city}</h1> |
| 43 | <p>{data.destination.name}</p> |
| 44 | <div className="trip-detail-dates"><CalendarDays size={16} /> {formatDateRange(data.startsOn, data.endsOn)}</div> |
| 45 | </div> |
| 46 | <div className="trip-detail-score"> |
| 47 | <ProgressRing value={data.culinaryCoverage} size="large" label="trip coverage" /> |
| 48 | <span>Tasted there during this trip</span> |
| 49 | </div> |
| 50 | <div className="trip-detail-actions"> |
| 51 | <button className="icon-button" type="button" onClick={() => setEditing(true)} aria-label="Edit trip"><Edit3 size={17} /></button> |
| 52 | <button className="icon-button icon-button-danger" type="button" disabled={remove.isPending} |
| 53 | onClick={() => window.confirm("Delete this trip and its mission? Your tasting journal stays intact.") && remove.mutate()} |
| 54 | aria-label="Delete trip"><Trash2 size={17} /></button> |
| 55 | </div> |
| 56 | </section> |
| 57 | |
| 58 | <section className="mission-sheet"> |
| 59 | <header> |
| 60 | <div> |
| 61 | <span className="eyebrow">Five-bite mission</span> |
| 62 | <h2>{data.missionCompleted === data.mission.length ? "Mission complete" : `${data.mission.length - data.missionCompleted} tastes left`}</h2> |
| 63 | </div> |
| 64 | <span className="mission-route"><MapPin size={15} /> Log them in {data.destination.name} during these dates</span> |
| 65 | </header> |
| 66 | <ol> |
| 67 | {data.mission.map(item => ( |
| 68 | <li key={item.dish.slug} className={item.completed ? "mission-complete" : ""}> |
| 69 | <span className="mission-sequence">{item.completed ? <Check size={18} /> : String(item.position).padStart(2, "0")}</span> |
| 70 | <div className="mission-dish-copy"> |
| 71 | <span>{item.dish.categoryLabel}</span> |
| 72 | <h3>{item.dish.name}</h3> |
| 73 | <p>{item.dish.description}</p> |
| 74 | <small>{item.dish.whyItMatters}</small> |
| 75 | </div> |
| 76 | {item.completed |
| 77 | ? <span className="mission-done">Stamped</span> |
| 78 | : <button className="button button-outline" onClick={() => openTaste({ dish: item.dish })}><Plus size={16} /> Log</button>} |
| 79 | </li> |
| 80 | ))} |
| 81 | </ol> |
| 82 | </section> |
| 83 | {editing && <TripFormDialog trip={data} onClose={() => setEditing(false)} />} |
| 84 | </div> |
| 85 | ); |
| 86 | } |
| 87 | |