SPB Git forge

spb/resto-ka

Public

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

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%

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 1 mo ago (Aug 22, 2026) parent 6a43314

5 changed files +74 −3

modified frontend/src/api.ts +30 −0
@@ -241,6 +241,36 @@ export function fetchRestaurants(query: RestaurantQuery = {}) {
241 241 export const fetchRestaurant = (uid: string) =>
242 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 274 export const fetchFacets = (region?: string) =>
245 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 11 import { Link, useSearchParams } from "react-router-dom";
12 12 import {
13 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 16 } from "../api";
16 17 import { IcoBag, IcoLeaf, IcoPin, IcoPlate, IcoSearch, IcoTag } from "../components/Icons";
17 18 import Pager from "../components/Pager";
@@ -67,6 +68,13 @@ export default function Home() {
67 68 const [page, setPage] = useState(0);
68 69 const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]);
69 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 79 // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à
72 80 // Escape (même pattern que le menu hamburger d'App.tsx)
@@ -330,6 +338,18 @@ export default function Home() {
330 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 353 <div className="results-head">
334 354 <h2>{mode === "plats" ? "Plats" : "Restaurants"}</h2>
335 355 <span>
modified frontend/src/pages/Resto.tsx +6 −2
@@ -10,7 +10,7 @@ import { useEffect, useState } from "react";
10 10 import { Link, useParams } from "react-router-dom";
11 11 import {
12 12 CONTEXT_LABELS, CONTEXT_SHORT, Menu, Restaurant, cuisineLabel, dietLabel,
13 − fetchRestaurant, fmtDate, fmtPrice, sourceName, typeLabel,
13 + fetchRestaurant, fmtDate, fmtPrice, pushRecentUid, sourceName, typeLabel,
14 14 } from "../api";
15 15 import { IcoCompass } from "../components/Icons";
16 16
@@ -110,7 +110,11 @@ export default function RestoPage() {
110 110 setResto(null);
111 111 setNotFound(false);
112 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 118 .catch(() => setNotFound(true));
115 119 window.scrollTo({ top: 0 });
116 120 }, [uid]);
modified frontend/src/styles.css +11 −0
@@ -223,6 +223,17 @@ img { display: block; }
223 223 }
224 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 237 .card {
227 238 background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);
228 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 116 chain: str | None = None,
117 117 source: str | None = None,
118 118 q: str | None = None,
119 + uids: str | None = None, # liste d'uids séparés par des virgules
119 120 has_menu: int | None = None, # 1 = seulement les restos avec menu
120 121 active: int = 1,
121 122 sort: str = "menu", # menu | name | price | recent
@@ -148,6 +149,11 @@ def list_restaurants(
148 149 if q:
149 150 sql += " AND (name LIKE ? OR address LIKE ? OR city LIKE ? OR chain LIKE ?)"
150 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 157 if has_menu == 1:
152 158 sql += " AND EXISTS (SELECT 1 FROM menus m WHERE m.uid=restaurants.uid)"
153 159 elif has_menu == 0:
154 160