profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
TripsPage.tsx 3,029 bytes
1 import { useState, type CSSProperties } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import { ArrowRight, CalendarDays, Check, MapPin, Plane, Plus } from "lucide-react";
4 import { Link } from "react-router-dom";
5 import { ProgressRing } from "../components/ProgressRing";
6 import { TripFormDialog } from "../components/TripFormDialog";
7 import { EmptyState, ErrorState, LoadingScreen, PageHeader, StatusPill } from "../components/Ui";
8 import { api } from "../lib/api";
9 import { formatDateRange } from "../lib/format";
10 import type { Trip } from "../types";
11
12 export function TripsPage() {
13 const [creating, setCreating] = useState(false);
14 const trips = useQuery({
15 queryKey: ["trips"],
16 queryFn: () => api<Trip[]>("/api/v1/trips")
17 });
18
19 if (trips.isLoading) return <LoadingScreen label="Opening your trips" />;
20 if (trips.isError) return <ErrorState message="Your trips could not be loaded." />;
21
22 return (
23 <div className="page trips-page">
24 <PageHeader
25 eyebrow="Travel plans"
26 title="Build the trip around five bites."
27 copy="A mission stays short on purpose. It gives you range without turning dinner into homework."
28 action={<button className="button button-primary" onClick={() => setCreating(true)}><Plus size={18} /> Add trip</button>}
29 />
30
31 {trips.data?.length ? (
32 <section className="trip-list" aria-label="Trips">
33 {trips.data.map(trip => (
34 <Link className="trip-card" key={trip.id} to={`/app/trips/${trip.id}`}
35 style={{ "--destination-accent": trip.destination.accentColor } as CSSProperties}>
36 <div className="trip-card-route">
37 <span>TLL</span><i /><Plane size={17} /><i /><span>{trip.destination.code}</span>
38 </div>
39 <div className="trip-card-main">
40 <div>
41 <StatusPill status={trip.status} />
42 <h2>{trip.city}</h2>
43 <p>{trip.destination.name}</p>
44 </div>
45 <ProgressRing value={trip.culinaryCoverage} size="medium" label="trip coverage" />
46 </div>
47 <div className="trip-card-meta">
48 <span><CalendarDays size={14} /> {formatDateRange(trip.startsOn, trip.endsOn)}</span>
49 <span><Check size={14} /> {trip.missionCompleted}/{trip.mission.length} mission bites</span>
50 </div>
51 <footer>
52 <span>Open food mission</span><ArrowRight size={17} />
53 </footer>
54 </Link>
55 ))}
56 </section>
57 ) : (
58 <EmptyState
59 title="No food missions yet"
60 copy="Add a destination. Tasteprint will choose five different parts of its food culture for you."
61 action={<button className="button button-primary" onClick={() => setCreating(true)}><MapPin size={17} /> Add first trip</button>}
62 />
63 )}
64 {creating && <TripFormDialog onClose={() => setCreating(false)} />}
65 </div>
66 );
67 }
68