profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
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