SPB Git

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

accueil : section « Récemment consultés » — fiches restos vues (localStorage) affichées en premier ; API /api/restaurants?uids=

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 22, 2026) parent 6a43314 Browse files

5 changed files +74 −3

modified frontend/src/api.ts +30 −0
@@ -241,6 +241,36 @@ export function fetchRestaurants(query: RestaurantQuery = {}) {
241 export const fetchRestaurant = (uid: string) => 241 export const fetchRestaurant = (uid: string) =>
242 get<Restaurant>(`/api/restaurants/${encodeURIComponent(uid)}`); 242 get<Restaurant>(`/api/restaurants/${encodeURIComponent(uid)}`);
243 243
244+// --- récemment consultés (localStorage, par appareil) ---------------------------
245+
246+const RECENT_KEY = "restoka:recent";
247+const RECENT_MAX = 12;
248+
249+export function getRecentUids(): string[] {
250+ try {
251+ const raw = JSON.parse(localStorage.getItem(RECENT_KEY) ?? "[]");
252+ return Array.isArray(raw) ? raw.filter((u) => typeof u === "string") : [];
253+ } catch {
254+ return [];
255+ }
256+}
257+
258+export function pushRecentUid(uid: string) {
259+ try {
260+ const next = [uid, ...getRecentUids().filter((u) => u !== uid)].slice(0, RECENT_MAX);
261+ localStorage.setItem(RECENT_KEY, JSON.stringify(next));
262+ } catch { /* stockage indisponible (navigation privée) */ }
263+}
264+
265+/** Fiches des restos consultés, remises dans l'ordre de consultation. */
266+export async function fetchRecentRestaurants(uids: string[]): Promise<Restaurant[]> {
267+ if (uids.length === 0) return [];
268+ const params = new URLSearchParams({ uids: uids.join(","), limit: String(uids.length) });
269+ const r = await get<{ restaurants: Restaurant[] }>(`/api/restaurants?${params.toString()}`);
270+ const by = new Map(r.restaurants.map((x) => [x.uid, x]));
271+ return uids.map((u) => by.get(u)).filter((x): x is Restaurant => x != null);
272+}
273+
244 export const fetchFacets = (region?: string) => 274 export const fetchFacets = (region?: string) =>
245 get<Facets>(`/api/facets${region ? `?region=${encodeURIComponent(region)}` : ""}`); 275 get<Facets>(`/api/facets${region ? `?region=${encodeURIComponent(region)}` : ""}`);
246 276
modified frontend/src/pages/Home.tsx +21 −1
@@ -11,7 +11,8 @@ import { useEffect, useMemo, useState } from "react";
11 import { Link, useSearchParams } from "react-router-dom"; 11 import { Link, useSearchParams } from "react-router-dom";
12 import { 12 import {
13 CONTEXT_SHORT, Dish, Facets, Restaurant, cuisineLabel, dietLabel, 13 CONTEXT_SHORT, Dish, Facets, Restaurant, cuisineLabel, dietLabel,
14− fetchDishes, fetchFacets, fetchRestaurants, fetchStats, fmtPrice, typeLabel, 14+ fetchDishes, fetchFacets, fetchRecentRestaurants, fetchRestaurants,
15+ fetchStats, fmtPrice, getRecentUids, typeLabel,
15 } from "../api"; 16 } from "../api";
16 import { IcoBag, IcoLeaf, IcoPin, IcoPlate, IcoSearch, IcoTag } from "../components/Icons"; 17 import { IcoBag, IcoLeaf, IcoPin, IcoPlate, IcoSearch, IcoTag } from "../components/Icons";
17 import Pager from "../components/Pager"; 18 import Pager from "../components/Pager";
@@ -67,6 +68,13 @@ export default function Home() {
67 const [page, setPage] = useState(0); 68 const [page, setPage] = useState(0);
68 const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]); 69 const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]);
69 const [sheetOpen, setSheetOpen] = useState(false); 70 const [sheetOpen, setSheetOpen] = useState(false);
71+ const [recent, setRecent] = useState<Restaurant[]>([]);
72+
73+ useEffect(() => {
74+ fetchRecentRestaurants(getRecentUids().slice(0, 8))
75+ .then(setRecent)
76+ .catch(() => {});
77+ }, []);
70 78
71 // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à 79 // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à
72 // Escape (même pattern que le menu hamburger d'App.tsx) 80 // Escape (même pattern que le menu hamburger d'App.tsx)
@@ -330,6 +338,18 @@ export default function Home() {
330 </div> 338 </div>
331 )} 339 )}
332 340
341+ {page === 0 && recent.length > 0 && (
342+ <section aria-label="Récemment consultés">
343+ <div className="results-head">
344+ <h2>Récemment consultés</h2>
345+ <span>vos dernières fiches</span>
346+ </div>
347+ <div className="recent-row">
348+ {recent.map((r) => <RestoCard key={r.uid} r={r} />)}
349+ </div>
350+ </section>
351+ )}
352+
333 <div className="results-head"> 353 <div className="results-head">
334 <h2>{mode === "plats" ? "Plats" : "Restaurants"}</h2> 354 <h2>{mode === "plats" ? "Plats" : "Restaurants"}</h2>
335 <span> 355 <span>
modified frontend/src/pages/Resto.tsx +6 −2
@@ -10,7 +10,7 @@ import { useEffect, useState } from "react";
10 import { Link, useParams } from "react-router-dom"; 10 import { Link, useParams } from "react-router-dom";
11 import { 11 import {
12 CONTEXT_LABELS, CONTEXT_SHORT, Menu, Restaurant, cuisineLabel, dietLabel, 12 CONTEXT_LABELS, CONTEXT_SHORT, Menu, Restaurant, cuisineLabel, dietLabel,
13− fetchRestaurant, fmtDate, fmtPrice, sourceName, typeLabel, 13+ fetchRestaurant, fmtDate, fmtPrice, pushRecentUid, sourceName, typeLabel,
14 } from "../api"; 14 } from "../api";
15 import { IcoCompass } from "../components/Icons"; 15 import { IcoCompass } from "../components/Icons";
16 16
@@ -110,7 +110,11 @@ export default function RestoPage() {
110 setResto(null); 110 setResto(null);
111 setNotFound(false); 111 setNotFound(false);
112 fetchRestaurant(uid) 112 fetchRestaurant(uid)
113− .then((r) => { setResto(r); setCtx(r.menus?.[0]?.price_context ?? null); }) 113+ .then((r) => {
114+ setResto(r);
115+ setCtx(r.menus?.[0]?.price_context ?? null);
116+ pushRecentUid(r.uid);
117+ })
114 .catch(() => setNotFound(true)); 118 .catch(() => setNotFound(true));
115 window.scrollTo({ top: 0 }); 119 window.scrollTo({ top: 0 });
116 }, [uid]); 120 }, [uid]);
modified frontend/src/styles.css +11 −0
@@ -223,6 +223,17 @@ img { display: block; }
223 } 223 }
224 @media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 24px; } } 224 @media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 24px; } }
225 225
226+/* récemment consultés — bande horizontale de cartes, défilement latéral */
227+.recent-row {
228+ display: grid; grid-auto-flow: column; grid-auto-columns: 290px;
229+ gap: 18px; overflow-x: auto; padding-bottom: 12px;
230+ scroll-snap-type: x proximity;
231+}
232+.recent-row > .card { scroll-snap-align: start; }
233+@media (max-width: 640px) {
234+ .recent-row { grid-auto-columns: 82%; margin-right: -16px; padding-right: 16px; }
235+}
236+
226 .card { 237 .card {
227 background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card); 238 background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);
228 overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-off-soft); 239 overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-off-soft);
modified restoka/web.py +6 −0
@@ -116,6 +116,7 @@ def list_restaurants(
116 chain: str | None = None, 116 chain: str | None = None,
117 source: str | None = None, 117 source: str | None = None,
118 q: str | None = None, 118 q: str | None = None,
119+ uids: str | None = None, # liste d'uids séparés par des virgules
119 has_menu: int | None = None, # 1 = seulement les restos avec menu 120 has_menu: int | None = None, # 1 = seulement les restos avec menu
120 active: int = 1, 121 active: int = 1,
121 sort: str = "menu", # menu | name | price | recent 122 sort: str = "menu", # menu | name | price | recent
@@ -148,6 +149,11 @@ def list_restaurants(
148 if q: 149 if q:
149 sql += " AND (name LIKE ? OR address LIKE ? OR city LIKE ? OR chain LIKE ?)" 150 sql += " AND (name LIKE ? OR address LIKE ? OR city LIKE ? OR chain LIKE ?)"
150 args += [f"%{q}%"] * 4 151 args += [f"%{q}%"] * 4
152+ if uids:
153+ lst = [u.strip() for u in uids.split(",") if u.strip()][:24]
154+ if lst:
155+ sql += f" AND uid IN ({','.join('?' * len(lst))})"
156+ args += lst
151 if has_menu == 1: 157 if has_menu == 1:
152 sql += " AND EXISTS (SELECT 1 FROM menus m WHERE m.uid=restaurants.uid)" 158 sql += " AND EXISTS (SELECT 1 FROM menus m WHERE m.uid=restaurants.uid)"
153 elif has_menu == 0: 159 elif has_menu == 0:
154 160
Menu

Theme