profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
ExplorePage.tsx 4,175 bytes
1 import { useMemo, useState } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import { ArrowRight, Search } from "lucide-react";
4 import { Link } from "react-router-dom";
5 import { ProgressRing } from "../components/ProgressRing";
6 import { ErrorState, LoadingScreen, PageHeader } from "../components/Ui";
7 import { WorldMap } from "../components/WorldMap";
8 import { api } from "../lib/api";
9 import type { Dashboard } from "../types";
10
11 export function ExplorePage() {
12 const [query, setQuery] = useState("");
13 const [region, setRegion] = useState("ALL");
14 const dashboard = useQuery({
15 queryKey: ["dashboard"],
16 queryFn: () => api<Dashboard>("/api/v1/progress/dashboard")
17 });
18
19 const regions = useMemo(() => Array.from(new Set(
20 dashboard.data?.tasteprint.destinations.map(item => item.destination.regionName) ?? []
21 )).sort(), [dashboard.data]);
22
23 const filtered = useMemo(() => dashboard.data?.tasteprint.destinations.filter(item => {
24 const search = query.trim().toLowerCase();
25 const matchesQuery = !search
26 || item.destination.name.toLowerCase().includes(search)
27 || item.destination.localName.toLowerCase().includes(search)
28 || item.destination.regionName.toLowerCase().includes(search);
29 return matchesQuery && (region === "ALL" || item.destination.regionName === region);
30 }) ?? [], [dashboard.data, query, region]);
31
32 if (dashboard.isLoading) return <LoadingScreen label="Opening the food atlas" />;
33 if (dashboard.isError || !dashboard.data) return <ErrorState message="The food atlas could not be loaded." />;
34
35 return (
36 <div className="page explore-page">
37 <PageHeader
38 eyebrow="The current atlas"
39 title="Taste the world by culture, not by checklist."
40 copy="Each destination starts with six different parts of its food life. The catalog grows through local review, not popularity alone."
41 />
42
43 <section className="atlas-map-panel">
44 <WorldMap destinations={dashboard.data.tasteprint.destinations} />
45 <div className="atlas-map-copy">
46 <span className="eyebrow">Your map</span>
47 <strong>{dashboard.data.tasteprint.stats.countriesTasted} of {dashboard.data.tasteprint.destinations.length}</strong>
48 <p>food cultures started</p>
49 </div>
50 </section>
51
52 <div className="filter-bar">
53 <label className="search-field">
54 <Search size={17} />
55 <span className="sr-only">Search countries</span>
56 <input value={query} onChange={event => setQuery(event.target.value)} placeholder="Search country or region" />
57 </label>
58 <div className="filter-chips" aria-label="Filter by region">
59 <button className={region === "ALL" ? "active" : ""} onClick={() => setRegion("ALL")}>All</button>
60 {regions.map(item => (
61 <button key={item} className={region === item ? "active" : ""} onClick={() => setRegion(item)}>{item}</button>
62 ))}
63 </div>
64 </div>
65
66 <section className="destination-grid" aria-label="Food cultures">
67 {filtered.map(item => (
68 <Link className="destination-card" key={item.destination.code} to={`/app/destinations/${item.destination.code}`}>
69 <div className="destination-card-top" style={{ backgroundColor: item.destination.accentColor }}>
70 <span className="destination-code">{item.destination.code}</span>
71 <span className="destination-local">{item.destination.localName}</span>
72 <div className="destination-contours" aria-hidden="true" />
73 </div>
74 <div className="destination-card-body">
75 <div>
76 <span className="eyebrow">{item.destination.regionName}</span>
77 <h2>{item.destination.name}</h2>
78 </div>
79 <ProgressRing value={item.coverage} size="small" />
80 <p>{item.destination.summary}</p>
81 <footer>
82 <span>{item.triedDishes}/{item.totalDishes} core dishes</span>
83 <ArrowRight size={17} />
84 </footer>
85 </div>
86 </Link>
87 ))}
88 </section>
89 </div>
90 );
91 }
92