| 1 | <!-- |
1 | <!-- |
| 2 | Author: Simon-Pierre Boucher <contact@spboucher.ai> |
2 | Author: Simon-Pierre Boucher <contact@spboucher.ai> |
| 3 | File: index.html |
3 | File: index.html |
| 4 | − Desc: Frontend Sorti-Ka — recherche + fiches événement (SPA sans build, |
4 | + Desc: Frontend Sorti-Ka v2 « guide culturel » — SPA sans build, routeur |
| 5 | − routeur History API, consomme /api). Design system Groupe KA |
5 | + History API, consomme /api. Accueil éditorial par sections (À la une, |
| 6 | − « éditorial sharp » via ka-ui (src/ka/tokens.css, chargé en premier) : |
6 | + Ce soir, Fin de semaine, Gratuit, catégories, À découvrir), recherche |
| 7 | − papier #f5f3ee · encre #141814 · vert #1c5c41 · ACCENT FRAMBOISE |
7 | + centrale + filtres en bottom sheet, timeline de jours, vue Liste|Carte |
| 8 | − #d6336c · Space Grotesk / Inter / JetBrains Mono · bordures encre + |
8 | + (Leaflet chargé à la demande), fiches image-first, CTA sticky mobile. |
| 9 | − ombres décalées + grain de film. Footer commun KaFooter et page |
9 | + Design system Groupe KA via ka-ui (src/ka/tokens.css, chargé en |
| |
10 | + premier) : papier #f5f3ee · encre #141814 · ACCENT FRAMBOISE #d6336c |
| |
11 | + (signal culturel : dates, états actifs, CTA) · Space Grotesk / Inter / |
| |
12 | + JetBrains Mono · grain de film. Footer commun KaFooter et page |
| 10 | /contact : contenu issu de src/ka/ecosystem.json (source de vérité — |
13 | /contact : contenu issu de src/ka/ecosystem.json (source de vérité — |
| 11 | resynchroniser le HTML si le JSON change). SSR : sortika/seo.py |
14 | resynchroniser le HTML si le JSON change). SSR : sortika/seo.py |
| 12 | injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY. |
15 | injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY. |
| 58 | .btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)} |
61 | .btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)} |
| 59 | .btn-ghost:hover{background:var(--accent-soft)} |
62 | .btn-ghost:hover{background:var(--accent-soft)} |
| 60 | |
63 | |
| 61 | −/* ---------- header / ticker ---------- */ |
64 | +/* ---------- header / ticker (compact 2026-08-25) ---------- */ |
| 62 | .ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden; |
65 | .ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden; |
| 63 | − font-family:var(--font-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase; |
66 | + font-family:var(--font-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase; |
| 64 | − padding:7px 0;white-space:nowrap} |
67 | + padding:6px 0;white-space:nowrap} |
| 65 | .ticker-track{display:inline-flex;animation:ticker 48s linear infinite;will-change:transform} |
68 | .ticker-track{display:inline-flex;animation:ticker 48s linear infinite;will-change:transform} |
| 66 | .ticker span{padding:0 26px;position:relative} |
69 | .ticker span{padding:0 26px;position:relative} |
| 67 | .ticker span::after{content:"◆";position:absolute;right:-6px;color:var(--lime);font-size:8px;top:3px} |
70 | .ticker span::after{content:"◆";position:absolute;right:-6px;color:var(--lime);font-size:8px;top:3px} |
| 68 | @keyframes ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}} |
71 | @keyframes ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}} |
| 69 | @media(prefers-reduced-motion:reduce){.ticker-track{animation:none}*{transition-duration:.01ms!important}} |
72 | @media(prefers-reduced-motion:reduce){.ticker-track{animation:none}*{transition-duration:.01ms!important}} |
| 70 | −/* header opaque, échelle --z-* (standard header/menu KA 2026-08-22) */ |
73 | +/* header opaque, échelle --z-* (standard header/menu KA 2026-08-22) — |
| |
74 | + v2 : une seule rangée mince ; sur mobile le contenu événementiel doit |
| |
75 | + apparaître tout de suite → ticker masqué, badge/tag repliés dans le menu */ |
| 71 | .header{position:sticky;top:0;z-index:var(--z-header,500);background:var(--paper); |
76 | .header{position:sticky;top:0;z-index:var(--z-header,500);background:var(--paper); |
| 72 | border-bottom:1.5px solid var(--ink)} |
77 | border-bottom:1.5px solid var(--ink)} |
| 73 | −.header-inner{display:flex;align-items:center;flex-wrap:wrap;gap:6px 18px;height:auto;min-height:64px;padding:6px 0} |
78 | +.header-inner{display:flex;align-items:center;gap:14px;min-height:58px;padding:4px 0} |
| 74 | .header-inner .gk-badge{flex:none} |
79 | .header-inner .gk-badge{flex:none} |
| |
80 | +@media(max-width:980px){.header-inner .gk-badge{display:none}} |
| 75 | @media(max-width:760px){ |
81 | @media(max-width:760px){ |
| 76 | − .header-inner{min-height:56px;padding:8px 0;gap:6px 10px} |
82 | + .ticker{display:none} |
| 77 | − /* KA Nav v2 (2026-08-25) : la nav inline cède la place au hamburger + panneau |
83 | + .header-inner{min-height:52px;padding:4px 0;gap:10px} |
| 78 | − plein écran fixed — menu visible peu importe la position de scroll */ |
| |
| 79 | − .nav{display:none} |
| |
| 80 | − #auth{margin-left:auto} |
| |
| 81 | − .ka-mnav-btn{display:flex} |
| |
| 82 | } |
84 | } |
| 83 | −.brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em; |
85 | +/* le reste des règles mobiles du header est déclaré APRÈS toutes les règles de |
| 84 | − display:flex;align-items:baseline;line-height:1;cursor:pointer;position:relative} |
86 | + base (nav, mnav, btn-ka, user-chip) — voir bloc « header mobile » plus bas : |
| |
87 | + à spécificité égale, c'est la dernière déclaration qui gagne. */ |
| |
88 | +.brand{font-family:var(--font-display);font-weight:700;font-size:23px;letter-spacing:-.03em; |
| |
89 | + display:flex;align-items:baseline;line-height:1;cursor:pointer;position:relative;flex:none} |
| 85 | /* extension tactile : le logo (98×26) atteint >=44px de cible sans changer le rendu */ |
90 | /* extension tactile : le logo (98×26) atteint >=44px de cible sans changer le rendu */ |
| 86 | .brand::after{content:"";position:absolute;inset:-8px} |
91 | .brand::after{content:"";position:absolute;inset:-8px} |
| 87 | .brand .ka{background:var(--accent);color:var(--on-accent);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)} |
92 | .brand .ka{background:var(--accent);color:var(--on-accent);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)} |
| 88 | .brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase; |
93 | .brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase; |
| 89 | − letter-spacing:.08em;color:var(--ink-3);margin-left:8px} |
94 | + letter-spacing:.08em;color:var(--ink-3)} |
| 90 | −@media(max-width:900px){.brand-tag{display:none}} |
95 | +@media(max-width:1100px){.brand-tag{display:none}} |
| 91 | −.nav{margin-left:auto;display:flex;gap:6px} |
96 | +.nav{margin-left:auto;display:flex;gap:2px} |
| 92 | −.nav a{display:inline-flex;align-items:center;min-height:44px;padding:8px 14px; |
97 | +.nav a{display:inline-flex;align-items:center;min-height:44px;padding:8px 13px; |
| 93 | − border:1.5px solid transparent;border-radius:var(--r-ctl); |
98 | + border-radius:999px;font-family:var(--font-display);font-weight:700;font-size:13.5px; |
| 94 | − font-family:var(--font-display);font-weight:700;font-size:13.5px;transition:all .15s ease} |
99 | + color:var(--ink-2);transition:all .15s ease} |
| 95 | −.nav a:hover{border-color:var(--ink);background:var(--accent-soft)} |
100 | +.nav a:hover{background:var(--accent-soft);color:var(--ink)} |
| 96 | |
101 | |
| 97 | /* ===== KA Nav v2 — menu mobile plein écran (standard 2026-08-25) ===== |
102 | /* ===== KA Nav v2 — menu mobile plein écran (standard 2026-08-25) ===== |
| 98 | Panneau position:fixed inset:0 (z-modal) → visible peu importe le scroll. */ |
103 | Panneau position:fixed inset:0 (z-modal) → visible peu importe le scroll. */ |
| 155 | font-size:9.5px;font-weight:700;text-transform:uppercase;color:var(--danger);padding:2px} |
160 | font-size:9.5px;font-weight:700;text-transform:uppercase;color:var(--danger);padding:2px} |
| 156 | .user-chip .out:hover{text-decoration:underline} |
161 | .user-chip .out:hover{text-decoration:underline} |
| 157 | @media(max-width:640px){.user-chip .uname{max-width:80px}} |
162 | @media(max-width:640px){.user-chip .uname{max-width:80px}} |
| 158 | − |
163 | +/* ---- header mobile (KA Nav v2) — déclaré après TOUTES les règles de base ---- */ |
| 159 | −/* ---------- hero ---------- */ |
| |
| 160 | −.hero{padding:56px 0 20px} |
| |
| 161 | −@media(max-width:640px){.hero{padding:34px 0 12px}} |
| |
| 162 | −.hero h1{font-size:clamp(34px,5.2vw,58px);font-weight:700;line-height:1.05;max-width:940px;margin-top:16px} |
| |
| 163 | −.hero p.lede{color:var(--ink-2);font-size:16.5px;max-width:660px;margin:18px 0 0} |
| |
| 164 | −.stat-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px} |
| |
| 165 | −.stat-chip{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-ctl); |
| |
| 166 | − padding:8px 14px;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-2); |
| |
| 167 | − display:flex;gap:8px;align-items:center;box-shadow:3px 3px 0 rgba(20,24,20,.12)} |
| |
| 168 | −.stat-chip b{color:var(--ink);font-weight:700} |
| |
| 169 | −.stat-chip .pulse{width:8px;height:8px;border-radius:50%;background:var(--green); |
| |
| 170 | − box-shadow:0 0 0 4px var(--lime-soft);animation:pulse 2.4s ease infinite} |
| |
| 171 | −@keyframes pulse{50%{box-shadow:0 0 0 7px rgba(28,92,65,.16)}} |
| |
| 172 | − |
| |
| 173 | −/* ---------- filtres ---------- */ |
| |
| 174 | −.filterbar{padding:14px;margin:28px 0 6px} |
| |
| 175 | −.f-primary{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap} |
| |
| 176 | −.f-search{flex:1 1 240px;min-width:0;max-width:460px;display:flex;align-items:center;gap:8px; |
| |
| 177 | − border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface-2); |
| |
| 178 | − padding:0 14px;min-height:46px;transition:background .15s ease,box-shadow .15s ease} |
| |
| 179 | −.f-search:focus-within{background:var(--surface);box-shadow:3px 3px 0 var(--lime)} |
| |
| 180 | −.f-search input{border:none;background:none;outline:none;flex:1;min-width:0; |
| |
| 181 | − font-size:14px;color:var(--ink);font-family:inherit} |
| |
| 182 | −.f-ctl{display:flex;flex-direction:column;justify-content:center;gap:2px; |
| |
| 183 | − border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface); |
| |
| 184 | − padding:6px 14px 5px;min-height:46px;cursor:pointer;transition:box-shadow .15s ease} |
| |
| 185 | −.f-ctl:focus-within{box-shadow:3px 3px 0 var(--lime)} |
| |
| 186 | −.f-ctl select{border:none;background:none;outline:none;font-family:var(--font-display); |
| |
| 187 | − font-size:13.5px;font-weight:700;color:var(--ink);padding:0;margin-left:-2px;max-width:220px} |
| |
| 188 | −/* champs >=16px sur mobile : évite le zoom automatique d'iOS au focus */ |
| |
| 189 | @media(max-width:760px){ |
164 | @media(max-width:760px){ |
| 190 | − .f-search input{font-size:16px} |
165 | + .nav{display:none} |
| 191 | − .f-ctl select{font-size:16px} |
166 | + #auth{margin-left:auto} |
| |
167 | + .ka-mnav-btn{display:flex} |
| |
168 | + .ka-signup{display:none} |
| |
169 | + .btn-ka .tx{display:none} |
| |
170 | + .btn-ka{padding:8px 11px;min-height:36px} |
| |
171 | + .user-chip .kaid{display:none} |
| |
172 | + .user-chip .out{display:none} |
| |
173 | +} |
| |
174 | + |
| |
175 | +/* ---------- hero éditorial (mosaïque d'images réelles en fond) ---------- */ |
| |
176 | +.hero{position:relative;padding:clamp(30px,6vw,72px) 0 8px;overflow:clip} |
| |
177 | +@media(max-width:640px){ |
| |
178 | + .hero h1{font-size:clamp(30px,8.6vw,38px)} |
| |
179 | + .hero-cats{font-size:10px;line-height:1.9;margin-top:12px} |
| |
180 | + .hero-stats{font-size:13px;margin-top:10px;line-height:1.7} |
| 192 | } |
181 | } |
| |
182 | +.hero-mosaic{position:absolute;inset:-24px -30px auto;height:130%;z-index:0; |
| |
183 | + display:grid;grid-template-columns:repeat(6,1fr);grid-auto-rows:1fr;gap:8px; |
| |
184 | + transform:rotate(-2deg);opacity:.3;pointer-events:none;filter:saturate(.85)} |
| |
185 | +@media(max-width:640px){.hero-mosaic{grid-template-columns:repeat(3,1fr)}} |
| |
186 | +.hero-mosaic .tile{border-radius:12px;overflow:hidden;background:var(--accent-soft)} |
| |
187 | +.hero-mosaic img{width:100%;height:100%;object-fit:cover} |
| |
188 | +.hero::after{content:"";position:absolute;inset:0;z-index:1; |
| |
189 | + background:linear-gradient(180deg,rgba(245,243,238,.55) 0%,rgba(245,243,238,.86) 46%,var(--paper) 96%)} |
| |
190 | +.hero .container{position:relative;z-index:2} |
| |
191 | +.hero h1{font-size:clamp(34px,5.4vw,60px);font-weight:700;line-height:1.04;max-width:940px;margin-top:14px} |
| |
192 | +.hero-cats{font-family:var(--font-mono);font-size:11.5px;font-weight:500;text-transform:uppercase; |
| |
193 | + letter-spacing:.13em;color:var(--ink-2);margin:16px 0 0;line-height:2} |
| |
194 | +.hero-cats i{font-style:normal;color:var(--accent);font-weight:700;padding:0 3px} |
| |
195 | +.hero-stats{font-size:14.5px;color:var(--ink-2);margin:14px 0 0;max-width:720px;line-height:1.8} |
| |
196 | +.hero-stats b{font-family:var(--font-display);font-size:1.35em;font-weight:700; |
| |
197 | + letter-spacing:-.02em;color:var(--ink);padding-right:1px} |
| |
198 | +.hero-stats i{font-style:normal;color:var(--accent);font-weight:700;padding:0 4px} |
| |
199 | + |
| |
200 | +/* ---------- recherche au centre + accès rapides + timeline de jours ---------- */ |
| |
201 | +.searchzone{margin-top:14px} |
| |
202 | +.searchbar{display:flex;align-items:center;gap:11px;background:var(--surface); |
| |
203 | + border:1.5px solid var(--ink);border-radius:16px;min-height:56px;padding:0 8px 0 17px; |
| |
204 | + box-shadow:var(--shadow-off-soft);transition:box-shadow .18s ease,transform .18s ease} |
| |
205 | +.searchbar:focus-within{box-shadow:4px 4px 0 var(--accent);transform:translate(-1px,-1px)} |
| |
206 | +.searchbar .sico{flex:none;width:19px;height:19px;color:var(--ink-3)} |
| |
207 | +.searchbar input{flex:1;min-width:0;border:0;background:none;outline:none; |
| |
208 | + font-size:16px;font-family:inherit;color:var(--ink);padding:14px 0} |
| |
209 | +.searchbar input::placeholder{color:var(--ink-3)} |
| |
210 | +.searchbar input::-webkit-search-cancel-button{display:none} |
| |
211 | +.sclear{flex:none;width:38px;height:38px;border-radius:50%;border:0;background:var(--surface-2); |
| |
212 | + color:var(--ink-2);font-size:13px;cursor:pointer;line-height:1} |
| |
213 | +.sclear:hover{background:var(--accent-soft);color:var(--ink)} |
| |
214 | +.quick{display:flex;gap:8px;margin-top:12px;overflow-x:auto;scrollbar-width:none;padding:2px} |
| |
215 | +.quick::-webkit-scrollbar{display:none} |
| |
216 | +@media(max-width:640px){.quick{margin-left:-16px;margin-right:-16px;padding:2px 16px}} |
| |
217 | +.qchip{flex:none;display:inline-flex;align-items:center;gap:7px;min-height:42px;padding:9px 16px; |
| |
218 | + border:1.5px solid rgba(20,24,20,.35);border-radius:999px;background:var(--surface); |
| |
219 | + font-family:var(--font-display);font-weight:700;font-size:13.5px;color:var(--ink); |
| |
220 | + cursor:pointer;transition:all .14s ease;white-space:nowrap} |
| |
221 | +.qchip svg{width:15px;height:15px;flex:none} |
| |
222 | +.qchip:hover{border-color:var(--ink);background:var(--accent-soft)} |
| |
223 | +.qchip.on{background:var(--accent);border-color:var(--accent-deep);color:#fff; |
| |
224 | + box-shadow:2px 2px 0 rgba(20,24,20,.22)} |
| |
225 | +.qchip .fb{min-width:19px;height:19px;border-radius:10px;background:var(--accent);color:#fff; |
| |
226 | + font-family:var(--font-mono);font-size:10.5px;font-weight:700; |
| |
227 | + display:inline-flex;align-items:center;justify-content:center;padding:0 5px} |
| |
228 | +.qchip.on .fb{background:#fff;color:var(--accent-deep)} |
| |
229 | +.days{display:flex;gap:8px;overflow-x:auto;margin-top:14px;padding:3px 2px 10px; |
| |
230 | + scroll-snap-type:x proximity;scrollbar-width:none} |
| |
231 | +.days::-webkit-scrollbar{display:none} |
| |
232 | +@media(max-width:640px){.days{margin-left:-16px;margin-right:-16px;padding:3px 16px 10px}} |
| |
233 | +.day{flex:none;scroll-snap-align:start;min-width:60px;padding:8px 6px 7px; |
| |
234 | + border:1.5px solid var(--line);border-radius:13px;background:var(--surface); |
| |
235 | + text-align:center;cursor:pointer;transition:all .14s ease} |
| |
236 | +.day small{display:block;font-family:var(--font-mono);font-size:9px;font-weight:700; |
| |
237 | + text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)} |
| |
238 | +.day b{display:block;font-family:var(--font-display);font-size:19px;line-height:1.2;color:var(--ink)} |
| |
239 | +.day i{display:block;font-style:normal;font-family:var(--font-mono);font-size:8.5px; |
| |
240 | + text-transform:uppercase;letter-spacing:.05em;color:var(--ink-3)} |
| |
241 | +.day:hover{border-color:var(--ink)} |
| |
242 | +.day.on{background:var(--accent);border-color:var(--accent-deep); |
| |
243 | + box-shadow:2px 2px 0 rgba(20,24,20,.22)} |
| |
244 | +.day.on small,.day.on i{color:rgba(255,255,255,.85)} |
| |
245 | +.day.on b{color:#fff} |
| |
246 | +.day.all b{font-size:13px;padding:6px 4px 5px;letter-spacing:0} |
| |
247 | + |
| |
248 | +/* chips génériques (filtres du sheet + périodes de /stats) */ |
| 193 | .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px} |
249 | .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px} |
| 194 | −.chip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl); |
250 | +.chip{border:1.5px solid rgba(20,24,20,.4);background:var(--surface);border-radius:999px; |
| 195 | − padding:6px 12px;font-family:var(--font-display);font-size:12.5px;font-weight:700; |
251 | + padding:8px 14px;font-family:var(--font-display);font-size:12.5px;font-weight:700; |
| 196 | − text-transform:none;letter-spacing:0;min-height:34px; |
252 | + text-transform:none;letter-spacing:0;min-height:38px;display:inline-flex;align-items:center;gap:6px; |
| 197 | cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease} |
253 | cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease} |
| 198 | −.chip:hover{background:var(--accent-soft)} |
254 | +.chip:hover{background:var(--accent-soft);border-color:var(--ink)} |
| 199 | −.chip.on{background:var(--ink);color:var(--accent);box-shadow:3px 3px 0 rgba(20,24,20,.25)} |
255 | +.chip.on{background:var(--ink);border-color:var(--ink);color:var(--accent);box-shadow:2px 2px 0 rgba(20,24,20,.25)} |
| 200 | .chip.free.on{background:var(--green);border-color:var(--green);color:#fff} |
256 | .chip.free.on{background:var(--green);border-color:var(--green);color:#fff} |
| 201 | |
257 | |
| 202 | −/* ---------- résultats ---------- */ |
258 | +/* ---------- sections éditoriales de l'accueil ---------- */ |
| 203 | −.meta-line{display:flex;align-items:center;gap:10px;margin:20px 2px 10px; |
259 | +.sec{margin:36px 0 6px} |
| |
260 | +.sec:first-child{margin-top:26px} |
| |
261 | +.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin:0 0 15px} |
| |
262 | +.sec-head .sk{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700; |
| |
263 | + text-transform:uppercase;letter-spacing:.12em;color:var(--accent-deep);margin-bottom:5px} |
| |
264 | +.sec-head h2{font-size:clamp(21px,2.7vw,31px);letter-spacing:-.025em;line-height:1.1;margin:0} |
| |
265 | +.sec-all{flex:none;font-family:var(--font-mono);font-size:11px;font-weight:700;text-transform:uppercase; |
| |
266 | + letter-spacing:.06em;color:var(--accent-deep);cursor:pointer;background:none;border:0;padding:0; |
| |
267 | + min-height:44px;display:inline-flex;align-items:center;gap:5px} |
| |
268 | +.sec-all:hover{text-decoration:underline;text-underline-offset:4px} |
| |
269 | + |
| |
270 | +/* ---------- fiches événement v2 — l'image domine ---------- */ |
| |
271 | +.ev{display:flex;flex-direction:column;gap:10px;cursor:pointer;min-width:0} |
| |
272 | +.ev-img{position:relative;aspect-ratio:4/3;border-radius:15px;overflow:hidden; |
| |
273 | + background:linear-gradient(135deg,var(--accent-soft) 0%,var(--surface-2) 70%)} |
| |
274 | +.ev-img::after{content:"";position:absolute;inset:0;border-radius:inherit; |
| |
275 | + border:1px solid rgba(20,24,20,.16);pointer-events:none} |
| |
276 | +.ev-img img{width:100%;height:100%;object-fit:cover;transition:transform .4s ease} |
| |
277 | +.ev-img.noimg::before{content:"Sorti·Ka";position:absolute;inset:auto 10px 8px auto; |
| |
278 | + font-family:var(--font-display);font-weight:700;font-size:13px;color:rgba(161,37,81,.4); |
| |
279 | + transform:rotate(-2deg)} |
| |
280 | +@media(hover:hover){ |
| |
281 | + .ev:hover .ev-img img{transform:scale(1.05)} |
| |
282 | + .ev:hover h3{text-decoration:underline;text-decoration-color:var(--accent); |
| |
283 | + text-decoration-thickness:2px;text-underline-offset:3px} |
| |
284 | + .feat:hover img{transform:scale(1.03)} |
| |
285 | +} |
| |
286 | +.ev-body{display:flex;flex-direction:column;gap:4px;min-width:0} |
| |
287 | +.ev-date{font-family:var(--font-mono);font-size:11px;font-weight:700;text-transform:uppercase; |
| |
288 | + letter-spacing:.05em;color:var(--accent-deep)} |
| |
289 | +.ev h3{font-family:var(--font-display);font-size:16.5px;font-weight:700;line-height:1.22; |
| |
290 | + letter-spacing:-.015em;margin:0;display:-webkit-box;-webkit-line-clamp:2; |
| |
291 | + -webkit-box-orient:vertical;overflow:hidden} |
| |
292 | +.ev-where{font-size:12.5px;color:var(--ink-2);margin:0; |
| |
293 | + white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| |
294 | +.ev-meta{display:flex;align-items:center;gap:10px;margin:2px 0 0;font-family:var(--font-mono); |
| |
295 | + font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em; |
| |
296 | + color:var(--ink-3);white-space:nowrap;overflow:hidden} |
| |
297 | +.ev-meta .src{color:rgba(139,146,140,.75);font-weight:500;overflow:hidden;text-overflow:ellipsis} |
| |
298 | +.ev-tag{position:absolute;left:10px;bottom:10px;z-index:2;font-family:var(--font-mono); |
| |
299 | + font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em; |
| |
300 | + background:rgba(20,24,20,.82);color:#fff;border-radius:9px;padding:5px 10px; |
| |
301 | + backdrop-filter:blur(3px)} |
| |
302 | +.ev-tag.free{background:var(--green)} |
| |
303 | +.ev-tag.stat-cancelled{background:#b3261e} |
| |
304 | +.ev-tag.stat-soldout{background:#5f6368} |
| |
305 | +.ev-tag.stat-postponed{background:#a05a00} |
| |
306 | + |
| |
307 | +/* grand événement vedette (À la une) */ |
| |
308 | +.feat{position:relative;display:block;border-radius:20px;overflow:hidden; |
| |
309 | + aspect-ratio:16/8.5;min-height:300px;cursor:pointer;background:var(--ink); |
| |
310 | + box-shadow:var(--shadow-off-soft);margin-bottom:16px} |
| |
311 | +@media(max-width:640px){.feat{aspect-ratio:4/4.6;min-height:0;border-radius:17px}} |
| |
312 | +.feat img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover; |
| |
313 | + transition:transform .5s ease} |
| |
314 | +.feat::before{content:"";position:absolute;inset:0;z-index:1; |
| |
315 | + background:linear-gradient(198deg,rgba(20,24,20,0) 34%,rgba(20,24,20,.55) 62%,rgba(20,24,20,.88) 96%)} |
| |
316 | +.feat-kick{position:absolute;top:14px;left:14px;z-index:2;font-family:var(--font-mono); |
| |
317 | + font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em; |
| |
318 | + background:var(--paper);color:var(--accent-deep);border-radius:999px;padding:6px 12px} |
| |
319 | +.feat .fav-btn{top:12px;right:12px} |
| |
320 | +.feat-body{position:absolute;inset:auto 0 0 0;z-index:2;padding:20px 22px 22px;color:#fff} |
| |
321 | +.feat-date{display:inline-flex;background:var(--accent);color:#fff;border-radius:9px; |
| |
322 | + padding:6px 11px;font-family:var(--font-mono);font-size:11px;font-weight:700; |
| |
323 | + text-transform:uppercase;letter-spacing:.05em;margin-bottom:11px} |
| |
324 | +.feat h3{color:#fff;font-size:clamp(22px,3.6vw,36px);line-height:1.07;letter-spacing:-.02em; |
| |
325 | + margin:0 0 7px;max-width:760px;display:-webkit-box;-webkit-line-clamp:3; |
| |
326 | + -webkit-box-orient:vertical;overflow:hidden} |
| |
327 | +.feat-where{font-size:13.5px;color:rgba(255,255,255,.85);margin:0; |
| |
328 | + white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| |
329 | +.feat-where b{color:#fff;font-weight:600} |
| |
330 | + |
| |
331 | +/* carrousel horizontal (scroll-snap, débord plein écran sur mobile) */ |
| |
332 | +.hscroll{display:grid;grid-auto-flow:column;grid-auto-columns:min(44vw,238px);gap:15px; |
| |
333 | + overflow-x:auto;scroll-snap-type:x mandatory;padding:4px 2px 12px;scrollbar-width:none} |
| |
334 | +.hscroll::-webkit-scrollbar{display:none} |
| |
335 | +.hscroll .ev{scroll-snap-align:start} |
| |
336 | +@media(max-width:640px){ |
| |
337 | + .hscroll{grid-auto-columns:60vw;margin-left:-16px;margin-right:-16px;padding:4px 16px 12px} |
| |
338 | +} |
| |
339 | + |
| |
340 | +/* liste compacte (À découvrir) */ |
| |
341 | +.compact{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2px 26px} |
| |
342 | +.evc{display:flex;gap:13px;align-items:center;padding:11px 0;min-width:0;cursor:pointer; |
| |
343 | + border-bottom:1px dashed var(--line)} |
| |
344 | +.evc-img{position:relative;flex:none;width:76px;height:76px;border-radius:13px;overflow:hidden; |
| |
345 | + background:linear-gradient(135deg,var(--accent-soft),var(--surface-2))} |
| |
346 | +.evc-img::after{content:"";position:absolute;inset:0;border-radius:inherit; |
| |
347 | + border:1px solid rgba(20,24,20,.14)} |
| |
348 | +.evc-img img{width:100%;height:100%;object-fit:cover} |
| |
349 | +.evc-body{min-width:0;flex:1} |
| |
350 | +.evc .ev-date{font-size:10px} |
| |
351 | +.evc h3{font-family:var(--font-display);font-size:14.5px;font-weight:700;line-height:1.25; |
| |
352 | + letter-spacing:-.01em;margin:1px 0 2px;display:-webkit-box;-webkit-line-clamp:2; |
| |
353 | + -webkit-box-orient:vertical;overflow:hidden} |
| |
354 | +.evc .ev-where{font-size:12px} |
| |
355 | + |
| |
356 | +/* ---------- résultats : compteur, pilules actives, Liste|Carte ---------- */ |
| |
357 | +.rtool{display:flex;align-items:center;justify-content:space-between;gap:12px; |
| |
358 | + flex-wrap:wrap;margin:20px 2px 8px} |
| |
359 | +.meta-line{display:flex;align-items:center;gap:10px; |
| 204 | font-family:var(--font-mono);font-size:11.5px;text-transform:uppercase; |
360 | font-family:var(--font-mono);font-size:11.5px;text-transform:uppercase; |
| 205 | letter-spacing:.08em;color:var(--ink-3)} |
361 | letter-spacing:.08em;color:var(--ink-3)} |
| 206 | .meta-line b{color:var(--ink)} |
362 | .meta-line b{color:var(--ink)} |
| 207 | −.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;margin:8px 0 34px} |
363 | +.afilters{display:flex;flex-wrap:wrap;gap:7px;margin:2px 2px 6px} |
| 208 | −.card{overflow:hidden;display:flex;flex-direction:column;cursor:pointer} |
364 | +.apill{display:inline-flex;align-items:center;gap:8px;min-height:34px;padding:5px 7px 5px 13px; |
| 209 | −.card-img{position:relative;height:150px;border-bottom:1.5px solid var(--ink); |
365 | + border:1.5px solid var(--accent-deep);border-radius:999px;background:var(--accent-soft); |
| 210 | − background:repeating-linear-gradient(-45deg,var(--lime-soft),var(--lime-soft) 12px,var(--surface-2) 12px,var(--surface-2) 24px);overflow:hidden} |
366 | + font-family:var(--font-display);font-weight:700;font-size:12px;color:var(--accent-deep); |
| 211 | −.card-img img{width:100%;height:100%;object-fit:cover} |
367 | + cursor:pointer;transition:all .12s ease} |
| 212 | −.badge-date{position:absolute;left:10px;bottom:10px;font-family:var(--font-mono); |
368 | +.apill i{font-style:normal;width:20px;height:20px;border-radius:50%;background:var(--accent-deep); |
| 213 | − font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em; |
369 | + color:#fff;font-size:10px;display:inline-flex;align-items:center;justify-content:center} |
| 214 | − color:var(--on-accent);background:var(--accent);border:1.5px solid var(--ink); |
370 | +.apill:hover{background:var(--accent);color:#fff} |
| 215 | − border-radius:var(--r-ctl);padding:4px 9px;white-space:nowrap;box-shadow:2px 2px 0 var(--ink)} |
371 | +.vtoggle{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px; |
| 216 | −.card-body{padding:15px 16px 14px;display:flex;flex-direction:column;gap:8px;flex:1} |
372 | + background:var(--surface);padding:3px;box-shadow:2px 2px 0 rgba(20,24,20,.12)} |
| 217 | −.card-top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start} |
373 | +.vtoggle button{border:0;background:none;min-height:36px;padding:6px 16px;border-radius:999px; |
| |
374 | + font-family:var(--font-display);font-weight:700;font-size:12.5px;cursor:pointer; |
| |
375 | + display:inline-flex;gap:7px;align-items:center;color:var(--ink-2);transition:all .14s ease} |
| |
376 | +.vtoggle button svg{width:13px;height:13px} |
| |
377 | +.vtoggle button.on{background:var(--ink);color:var(--paper)} |
| |
378 | +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(236px,1fr));gap:26px 20px;margin:12px 0 34px} |
| |
379 | +@media(max-width:640px){.grid{grid-template-columns:1fr 1fr;gap:22px 12px} |
| |
380 | + .grid .ev h3{font-size:14.5px}} |
| |
381 | + |
| |
382 | +/* ---------- carte (Leaflet, chargé à la demande) ---------- */ |
| |
383 | +.mapwrap{position:relative;isolation:isolate;border:1.5px solid var(--ink);border-radius:17px; |
| |
384 | + overflow:hidden;height:min(68dvh,640px);margin:12px 0 44px;background:var(--surface-2)} |
| |
385 | +@media(max-width:760px){.mapwrap{height:calc(var(--vh100, 100dvh) - 190px);margin:10px 0 30px}} |
| |
386 | +#map{position:absolute;inset:0;z-index:0} |
| |
387 | +.map-note{position:absolute;left:12px;bottom:12px;z-index:1200;font-family:var(--font-mono); |
| |
388 | + font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em; |
| |
389 | + background:rgba(255,255,255,.92);border:1px solid rgba(20,24,20,.3);color:var(--ink-2); |
| |
390 | + border-radius:999px;padding:6px 12px;pointer-events:none;max-width:70%} |
| |
391 | +.map-load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; |
| |
392 | + z-index:1100;font-family:var(--font-mono);font-size:11px;text-transform:uppercase; |
| |
393 | + letter-spacing:.08em;color:var(--ink-3);background:var(--surface-2)} |
| |
394 | +.leaflet-popup-content-wrapper{border-radius:14px;border:1.5px solid var(--ink); |
| |
395 | + box-shadow:4px 4px 0 rgba(20,24,20,.18);font-family:var(--font-body)} |
| |
396 | +.leaflet-popup-content{margin:0;width:220px!important} |
| |
397 | +.mpop{display:block;color:var(--ink);text-decoration:none} |
| |
398 | +.mpop img{width:100%;height:104px;object-fit:cover;border-radius:12px 12px 0 0} |
| |
399 | +.mpop .mp-b{padding:10px 13px 12px} |
| |
400 | +.mpop .ev-date{font-size:10px} |
| |
401 | +.mpop h4{font-family:var(--font-display);font-size:13.5px;font-weight:700;line-height:1.25; |
| |
402 | + margin:3px 0 3px;letter-spacing:-.01em} |
| |
403 | +.mpop p{font-size:11.5px;color:var(--ink-2);margin:0;white-space:nowrap; |
| |
404 | + overflow:hidden;text-overflow:ellipsis} |
| |
405 | + |
| |
406 | +/* badges conservés (fiche événement + bannières de statut) */ |
| 218 | .badge-free{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
407 | .badge-free{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
| 219 | − color:#fff;background:var(--green);border-radius:var(--r-ctl);padding:4px 8px;flex:none} |
408 | + color:#fff;background:var(--green);border-radius:7px;padding:4px 8px;flex:none} |
| 220 | .badge-price{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
409 | .badge-price{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
| 221 | color:var(--ink);background:var(--amber-soft);border:1px solid var(--amber); |
410 | color:var(--ink);background:var(--amber-soft);border:1px solid var(--amber); |
| 222 | − border-radius:var(--r-ctl);padding:3px 8px;flex:none} |
411 | + border-radius:7px;padding:3px 8px;flex:none} |
| 223 | .badge-status{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
412 | .badge-status{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase; |
| 224 | − color:#fff;background:#b3261e;border:1px solid var(--ink); |
413 | + color:#fff;background:#b3261e;border-radius:7px;padding:4px 8px;flex:none} |
| 225 | − border-radius:var(--r-ctl);padding:4px 8px;flex:none} |
| |
| 226 | .badge-status--soldout{background:#5f6368} |
414 | .badge-status--soldout{background:#5f6368} |
| 227 | .badge-status--postponed{background:#a05a00} |
415 | .badge-status--postponed{background:#a05a00} |
| 228 | −.status-banner{margin:0 0 14px;padding:10px 14px;border:1.5px solid var(--ink); |
416 | +.status-banner{margin:0 0 14px;padding:11px 15px;border-radius:12px;font-weight:700; |
| 229 | − border-radius:var(--r-ctl);font-weight:700;color:#fff;background:#b3261e; |
417 | + color:#fff;background:#b3261e} |
| 230 | − box-shadow:2px 2px 0 var(--ink)} |
| |
| 231 | .status-banner--soldout{background:#5f6368} |
418 | .status-banner--soldout{background:#5f6368} |
| 232 | .status-banner--postponed{background:#a05a00} |
419 | .status-banner--postponed{background:#a05a00} |
| 233 | −.card h3{font-size:17px;font-weight:700;line-height:1.25} |
| |
| 234 | −.card .desc{font-size:12.5px;color:var(--ink-2);display:-webkit-box; |
| |
| 235 | − -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} |
| |
| 236 | −.card .where{font-size:12.5px;color:var(--ink-3)} |
| |
| 237 | −.card .where b{color:var(--ink-2);font-weight:600} |
| |
| 238 | −.card .cats{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:4px} |
| |
| 239 | .cat{font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.06em; |
420 | .cat{font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.06em; |
| 240 | text-transform:uppercase;color:var(--ink-2);border:1px solid var(--line-strong); |
421 | text-transform:uppercase;color:var(--ink-2);border:1px solid var(--line-strong); |
| 241 | − border-radius:var(--r-ctl);padding:2px 7px} |
422 | + border-radius:999px;padding:3px 9px} |
| 242 | −.card .src{font-family:var(--font-mono);font-size:10px;text-transform:uppercase; |
| |
| 243 | − letter-spacing:.04em;color:var(--ink-3);border-top:1px dashed var(--line-strong); |
| |
| 244 | − padding-top:10px;margin-top:6px;display:flex;justify-content:space-between} |
| |
| 245 | −.card .src .go{color:var(--green);font-weight:700} |
| |
| 246 | .more{display:block;margin:0 auto 54px} |
423 | .more{display:block;margin:0 auto 54px} |
| |
424 | +.more[hidden]{display:none} |
| 247 | .empty{padding:60px 10px;text-align:center;color:var(--ink-3);grid-column:1/-1} |
425 | .empty{padding:60px 10px;text-align:center;color:var(--ink-3);grid-column:1/-1} |
| 248 | .empty b{font-family:var(--font-display);font-size:19px;color:var(--ink);display:block;margin-bottom:6px} |
426 | .empty b{font-family:var(--font-display);font-size:19px;color:var(--ink);display:block;margin-bottom:6px} |
| 249 | |
427 | |
| 250 | −/* ---------- fiche événement ---------- */ |
428 | +/* ---------- fiche événement v2 ---------- */ |
| 251 | −.detail{padding:34px 0 50px} |
429 | +.detail{padding-top:24px;padding-bottom:60px} |
| 252 | .back{display:inline-flex;align-items:center;gap:8px;min-height:44px;font-family:var(--font-mono); |
430 | .back{display:inline-flex;align-items:center;gap:8px;min-height:44px;font-family:var(--font-mono); |
| 253 | font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em; |
431 | font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em; |
| 254 | − color:var(--green);cursor:pointer;margin-bottom:8px} |
432 | + color:var(--accent-deep);cursor:pointer;margin-bottom:6px} |
| 255 | @media(pointer:coarse){.chip{min-height:44px}.user-chip .out{min-height:32px;padding:2px 8px}} |
433 | @media(pointer:coarse){.chip{min-height:44px}.user-chip .out{min-height:32px;padding:2px 8px}} |
| 256 | .back:hover{text-decoration:underline} |
434 | .back:hover{text-decoration:underline} |
| 257 | −.detail-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:26px;align-items:start} |
435 | +.detail-grid{display:grid;grid-template-columns:1.55fr 1fr;gap:30px;align-items:start} |
| 258 | −@media(max-width:860px){.detail-grid{grid-template-columns:1fr}} |
436 | +@media(max-width:860px){.detail-grid{grid-template-columns:1fr;gap:20px}} |
| 259 | −.detail-hero{overflow:hidden} |
437 | +.detail-main{min-width:0} |
| 260 | −.detail-hero .img{height:340px;border-bottom:1.5px solid var(--ink); |
438 | +.d-imgwrap{position:relative;aspect-ratio:16/9;border-radius:19px;overflow:hidden; |
| 261 | − background:repeating-linear-gradient(-45deg,var(--lime-soft),var(--lime-soft) 14px,var(--surface-2) 14px,var(--surface-2) 28px)} |
439 | + background:linear-gradient(135deg,var(--accent-soft),var(--surface-2))} |
| 262 | −.detail-hero .img img{width:100%;height:100%;object-fit:cover} |
440 | +.d-imgwrap::after{content:"";position:absolute;inset:0;border-radius:inherit; |
| 263 | −.detail-hero .body{padding:24px 26px 26px} |
441 | + border:1px solid rgba(20,24,20,.18);pointer-events:none} |
| 264 | −.detail-hero h1{font-size:clamp(26px,3.4vw,40px);line-height:1.08;margin:10px 0 14px} |
442 | +.d-imgwrap img{width:100%;height:100%;object-fit:cover} |
| 265 | −.detail-hero .desc{color:var(--ink-2);font-size:15px;white-space:pre-line} |
443 | +@media(max-width:640px){.d-imgwrap{aspect-ratio:4/3;border-radius:15px;margin:0 0 2px}} |
| 266 | −.detail-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px} |
444 | +.d-body{padding:20px 2px 0} |
| 267 | −.side{display:flex;flex-direction:column;gap:16px} |
445 | +.d-body h1{font-size:clamp(26px,3.6vw,42px);line-height:1.07;margin:10px 0 14px} |
| 268 | −.side .box{padding:18px 20px} |
446 | +.d-body .desc{color:var(--ink-2);font-size:15px;white-space:pre-line;max-width:70ch} |
| 269 | −.fact{display:flex;justify-content:space-between;gap:14px;padding:9px 0;border-bottom:1px dashed var(--line-strong);font-size:13.5px} |
447 | +.detail-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px} |
| 270 | −.fact:last-child{border-bottom:none} |
448 | +.side{display:flex;flex-direction:column;gap:14px;position:sticky;top:76px} |
| |
449 | +@media(max-width:860px){.side{position:static}} |
| |
450 | +.side .box{background:var(--surface);border:1px solid rgba(20,24,20,.25);border-radius:16px; |
| |
451 | + padding:18px 20px;box-shadow:var(--shadow-flat)} |
| |
452 | +.fact{display:flex;justify-content:space-between;gap:14px;padding:10px 0; |
| |
453 | + border-bottom:1px dashed var(--line);font-size:13.5px} |
| |
454 | +.fact:first-child{padding-top:2px} |
| |
455 | +.fact:last-child{border-bottom:none;padding-bottom:2px} |
| 271 | .fact .k{font-family:var(--font-mono);font-size:10px;font-weight:700; |
456 | .fact .k{font-family:var(--font-mono);font-size:10px;font-weight:700; |
| 272 | text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding-top:3px;flex:none} |
457 | text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding-top:3px;flex:none} |
| 273 | .fact .v{text-align:right;font-weight:600} |
458 | .fact .v{text-align:right;font-weight:600} |
| 274 | .side .btn{width:100%} |
459 | .side .btn{width:100%} |
| 275 | −.maplink{font-family:var(--font-mono);font-size:11px;color:var(--green);font-weight:700;text-transform:uppercase} |
460 | +.btn-cta{background:var(--accent);color:#fff;border-color:var(--accent-deep); |
| |
461 | + box-shadow:4px 4px 0 rgba(161,37,81,.3)} |
| |
462 | +.btn-cta:hover{background:var(--accent-deep);color:#fff} |
| |
463 | +.maplink{font-family:var(--font-mono);font-size:11px;color:var(--accent-deep);font-weight:700;text-transform:uppercase} |
| 276 | .maplink:hover{text-decoration:underline} |
464 | .maplink:hover{text-decoration:underline} |
| 277 | −.similar h2{font-size:22px;margin:44px 0 4px} |
465 | +.similar h2{font-size:22px;margin:46px 0 4px} |
| |
466 | +/* barre d'action collée en bas — mobile seulement (billets toujours à un pouce) */ |
| |
467 | +.d-ctabar{display:none} |
| |
468 | +@media(max-width:860px){ |
| |
469 | + .d-ctabar{position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600); |
| |
470 | + display:flex;align-items:center;gap:14px;background:var(--surface); |
| |
471 | + border-top:1.5px solid var(--ink);padding:10px 16px calc(10px + env(safe-area-inset-bottom))} |
| |
472 | + .d-ctabar .p{flex:1;min-width:0} |
| |
473 | + .d-ctabar .p b{font-family:var(--font-display);font-size:15.5px;letter-spacing:-.01em; |
| |
474 | + display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| |
475 | + .d-ctabar .p span{display:block;font-family:var(--font-mono);font-size:9.5px; |
| |
476 | + text-transform:uppercase;letter-spacing:.05em;color:var(--accent-deep);margin-top:1px} |
| |
477 | + .d-ctabar .btn{flex:none;min-width:132px} |
| |
478 | + .detail{padding-bottom:120px} |
| |
479 | + /* le bouton ka-agent remonte au-dessus de la barre CTA sur la fiche mobile */ |
| |
480 | + body:has(#view-detail:not(.hidden)) .kaa-btn{ |
| |
481 | + bottom:calc(86px + env(safe-area-inset-bottom,0px))!important} |
| |
482 | +} |
| |
483 | +.d-imgwrap.noimg{aspect-ratio:16/5} |
| 278 | |
484 | |
| 279 | /* ---------- sources / footer ---------- */ |
485 | /* ---------- sources / footer ---------- */ |
| 280 | −.sources{background:var(--surface-2);border-top:1.5px solid var(--ink);padding:44px 0 38px} |
486 | +.sources{background:var(--surface-2);border-top:1px solid rgba(20,24,20,.35);padding:46px 0 40px;margin-top:26px} |
| 281 | .sources h2{font-size:26px;margin-top:12px} |
487 | .sources h2{font-size:26px;margin-top:12px} |
| 282 | .sources p.lede2{color:var(--ink-2);font-size:14.5px;max-width:720px;margin:10px 0 0} |
488 | .sources p.lede2{color:var(--ink-2);font-size:14.5px;max-width:720px;margin:10px 0 0} |
| 283 | −.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-top:22px} |
489 | +.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-top:24px} |
| 284 | −.src-card{padding:16px 18px} |
490 | +.src-card{background:var(--surface);border:1px solid rgba(20,24,20,.18);border-radius:14px; |
| 285 | −.src-card h4{font-size:15px;margin-bottom:5px} |
491 | + padding:15px 17px;box-shadow:var(--shadow-flat)} |
| |
492 | +.src-card h4{font-size:14.5px;margin-bottom:5px} |
| 286 | .src-card .lic{font-family:var(--font-mono);font-size:9.5px;font-weight:700; |
493 | .src-card .lic{font-family:var(--font-mono);font-size:9.5px;font-weight:700; |
| 287 | color:var(--green);text-transform:uppercase;letter-spacing:.06em} |
494 | color:var(--green);text-transform:uppercase;letter-spacing:.06em} |
| 288 | .src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px} |
495 | .src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px} |
| 289 | −.src-card .n b{color:var(--green-deep)} |
496 | +.src-card .n b{color:var(--accent-deep)} |
| 290 | .ka-footer{margin-top:0} |
497 | .ka-footer{margin-top:0} |
| 291 | |
498 | |
| 292 | /* ---------- page /contact ---------- */ |
499 | /* ---------- page /contact ---------- */ |
| 293 | −.contact-page{padding:44px 0 64px} |
500 | +.contact-page{padding-top:44px;padding-bottom:64px} |
| 294 | .contact-page h1{font-size:clamp(30px,4.2vw,48px);line-height:1.06;margin-top:16px;max-width:820px} |
501 | .contact-page h1{font-size:clamp(30px,4.2vw,48px);line-height:1.06;margin-top:16px;max-width:820px} |
| 295 | .contact-page .lede{color:var(--ink-2);font-size:16px;max-width:660px;margin:16px 0 0} |
502 | .contact-page .lede{color:var(--ink-2);font-size:16px;max-width:660px;margin:16px 0 0} |
| 296 | .contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr)); |
503 | .contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr)); |
| 314 | .contact-sites a small{font-family:var(--font-mono);font-weight:500;font-size:10px; |
521 | .contact-sites a small{font-family:var(--font-mono);font-weight:500;font-size:10px; |
| 315 | color:var(--ink-3);margin-left:8px} |
522 | color:var(--ink-3);margin-left:8px} |
| 316 | /* ---------- page /stats (module ka-stats, vanilla + SVG) ---------- */ |
523 | /* ---------- page /stats (module ka-stats, vanilla + SVG) ---------- */ |
| 317 | −.stats-page{padding:40px 0 60px} |
524 | +.stats-page{padding-top:40px;padding-bottom:60px} |
| 318 | −@media(max-width:640px){.stats-page{padding:28px 0 44px}} |
525 | +@media(max-width:640px){.stats-page{padding-top:28px;padding-bottom:44px}} |
| 319 | .st-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;justify-content:space-between} |
526 | .st-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;justify-content:space-between} |
| 320 | .st-head h1{font-size:clamp(28px,4vw,44px);line-height:1.06;margin-top:12px} |
527 | .st-head h1{font-size:clamp(28px,4vw,44px);line-height:1.06;margin-top:12px} |
| 321 | .st-fresh{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0 0} |
528 | .st-fresh{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0 0} |
| 455 | .d-title-row h1{min-width:0} |
662 | .d-title-row h1{min-width:0} |
| 456 | .fav-empty{grid-column:1/-1} |
663 | .fav-empty{grid-column:1/-1} |
| 457 | .fav-empty .btn-ka{margin-top:12px} |
664 | .fav-empty .btn-ka{margin-top:12px} |
| |
665 | +/* ---------- panneau de filtres : bottom sheet mobile / modale centrée desktop ---------- */ |
| |
666 | +.sheet-ov{position:fixed;inset:0;z-index:var(--z-overlay,800);background:rgba(20,24,20,.45); |
| |
667 | + opacity:0;transition:opacity .22s ease;pointer-events:none} |
| |
668 | +.sheet-ov.open{opacity:1;pointer-events:auto} |
| |
669 | +.sheet{position:fixed;left:0;right:0;bottom:0;z-index:var(--z-modal,900);background:var(--paper); |
| |
670 | + border:1.5px solid var(--ink);border-bottom:0;border-radius:22px 22px 0 0; |
| |
671 | + max-height:88dvh;display:flex;flex-direction:column; |
| |
672 | + transform:translateY(103%);transition:transform .3s cubic-bezier(.32,.72,.28,1); |
| |
673 | + visibility:hidden} |
| |
674 | +.sheet.open{transform:none;visibility:visible} |
| |
675 | +.sheet-grab{width:44px;height:5px;border-radius:3px;background:rgba(20,24,20,.22); |
| |
676 | + margin:10px auto 2px;flex:none} |
| |
677 | +.sheet-head{display:flex;justify-content:space-between;align-items:center; |
| |
678 | + padding:8px 20px 12px;border-bottom:1px solid var(--line);flex:none} |
| |
679 | +.sheet-head h2{font-size:17px;margin:0} |
| |
680 | +.sheet-x{width:38px;height:38px;border-radius:50%;border:1.5px solid var(--ink); |
| |
681 | + background:var(--surface);cursor:pointer;font-size:14px;line-height:1;padding:0; |
| |
682 | + display:inline-flex;align-items:center;justify-content:center} |
| |
683 | +.sheet-x:hover{background:var(--accent-soft)} |
| |
684 | +.sheet-body{overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain; |
| |
685 | + padding:18px 20px 12px} |
| |
686 | +.sheet-sec{margin:0 0 22px} |
| |
687 | +.sheet-sec>.klabel{display:block;margin-bottom:10px} |
| |
688 | +.sheet-chips{display:flex;flex-wrap:wrap;gap:8px} |
| |
689 | +.sheet .select{max-width:420px} |
| |
690 | +.sheet-dates{display:flex;gap:8px;align-items:center;margin-top:12px;flex-wrap:wrap} |
| |
691 | +.sheet-dates .input{width:158px;padding:9px 11px;font-size:14px;border-radius:11px} |
| |
692 | +.sheet-foot{display:flex;gap:10px;padding:12px 20px calc(14px + env(safe-area-inset-bottom)); |
| |
693 | + border-top:1.5px solid var(--ink);flex:none;background:var(--surface);border-radius:0} |
| |
694 | +.sheet-foot .btn{flex:1;border-radius:12px} |
| |
695 | +@media(min-width:761px){ |
| |
696 | + .sheet{left:50%;right:auto;bottom:auto;top:50%;width:min(620px,94vw);max-height:82vh; |
| |
697 | + border-radius:20px;border-bottom:1.5px solid var(--ink); |
| |
698 | + box-shadow:12px 12px 0 rgba(20,24,20,.16); |
| |
699 | + transform:translate(-50%,-46%) scale(.97);opacity:0; |
| |
700 | + transition:opacity .18s ease,transform .18s ease} |
| |
701 | + .sheet.open{opacity:1;transform:translate(-50%,-50%) scale(1)} |
| |
702 | + .sheet-grab{display:none} |
| |
703 | + .sheet-foot{border-radius:0 0 18px 18px} |
| |
704 | +} |
| |
705 | + |
| |
706 | +/* ---------- toast ---------- */ |
| |
707 | +.toast{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom)); |
| |
708 | + transform:translateX(-50%) translateY(10px);z-index:var(--z-toast,950); |
| |
709 | + background:var(--ink);color:var(--paper);border-radius:13px;padding:12px 19px; |
| |
710 | + font-size:13.5px;font-weight:600;opacity:0;transition:all .25s ease; |
| |
711 | + pointer-events:none;max-width:92vw;text-align:center;box-shadow:0 8px 24px rgba(20,24,20,.3)} |
| |
712 | +.toast.show{opacity:1;transform:translateX(-50%)} |
| |
713 | + |
| 458 | #ssr{display:none} |
714 | #ssr{display:none} |
| 459 | .hidden{display:none!important} |
715 | .hidden{display:none!important} |
| 460 | </style> |
716 | </style> |
| 480 | Un service <b>Groupe<span class="ka">KA</span></b></a> |
736 | Un service <b>Groupe<span class="ka">KA</span></b></a> |
| 481 | <span class="brand-tag">Sorties & événements — Québec</span> |
737 | <span class="brand-tag">Sorties & événements — Québec</span> |
| 482 | <nav class="nav"> |
738 | <nav class="nav"> |
| 483 | − <a href="/#sources">Sources</a> |
739 | + <a data-nav href="/">Sorties</a> |
| 484 | <a data-nav href="/favoris" title="Mes favoris — Mon univers Ka">♥ Favoris</a> |
740 | <a data-nav href="/favoris" title="Mes favoris — Mon univers Ka">♥ Favoris</a> |
| 485 | <a data-nav href="/stats">Stats</a> |
741 | <a data-nav href="/stats">Stats</a> |
| 486 | <a data-nav href="/contact">Contact</a> |
742 | <a data-nav href="/contact">Contact</a> |
| 487 | − <a href="/docs" target="_blank" rel="noopener">API</a> |
| |
| 488 | </nav> |
743 | </nav> |
| 489 | <span id="auth"></span> |
744 | <span id="auth"></span> |
| 490 | <button id="ka-mnav-btn" class="ka-mnav-btn" aria-label="Ouvrir le menu" |
745 | <button id="ka-mnav-btn" class="ka-mnav-btn" aria-label="Ouvrir le menu" |
| 520 | </div> |
775 | </div> |
| 521 | </div> |
776 | </div> |
| 522 | |
777 | |
| 523 | −<!-- ======== VUE LISTE ======== --> |
778 | +<!-- ======== VUE LISTE : accueil éditorial + résultats Liste|Carte ======== --> |
| 524 | <main id="view-list"> |
779 | <main id="view-list"> |
| 525 | − <section class="hero container"> |
780 | + <section class="hero"> |
| 526 | − <span class="kicker">Agrégateur indépendant · mise à jour horaire</span> |
781 | + <div class="hero-mosaic" id="hero-mosaic" aria-hidden="true"></div> |
| 527 | − <h1>Toutes les sorties du Québec, <span class="hl">un seul endroit.</span></h1> |
782 | + <div class="container"> |
| 528 | − <p class="lede">Concerts, festivals, spectacles, expositions, sport, famille — agrégés |
783 | + <span class="kicker">Agrégateur indépendant · mise à jour continue</span> |
| 529 | − depuis des billetteries et des sources ouvertes officielles, dans les 17 régions. |
784 | + <h1>Toutes les sorties du Québec, <span class="hl">un seul endroit.</span></h1> |
| 530 | − Chaque fiche renvoie vers la billetterie ou la source originale.</p> |
785 | + <p class="hero-cats">concerts <i>·</i> festivals <i>·</i> humour <i>·</i> théâtre |
| 531 | − <div class="stat-row" id="stats"></div> |
786 | + <i>·</i> expos <i>·</i> famille <i>·</i> sport</p> |
| |
787 | + <p class="hero-stats" id="stats"></p> |
| |
788 | + </div> |
| 532 | </section> |
789 | </section> |
| 533 | |
790 | |
| 534 | − <section class="container"> |
791 | + <section class="container searchzone"> |
| 535 | − <div class="filterbar gk-card"> |
792 | + <div class="searchbar" role="search"> |
| 536 | − <div class="f-primary"> |
793 | + <svg class="sico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" |
| 537 | − <label class="f-search">🔎<input type="text" id="q" placeholder="Artiste, événement, salle, ville…"></label> |
794 | + stroke-linecap="round" aria-hidden="true"> |
| 538 | − <label class="f-ctl"><span class="klabel">Région</span> |
795 | + <circle cx="11" cy="11" r="7"/><path d="m20 20-3.8-3.8"/></svg> |
| 539 | − <select id="region"><option value="">Toutes les régions</option></select> |
796 | + <input type="search" id="q" placeholder="Artiste, événement, salle ou ville…" |
| 540 | − </label> |
797 | + autocomplete="off" aria-label="Rechercher une sortie"> |
| 541 | − <label class="f-ctl"><span class="klabel">Quand</span> |
798 | + <button class="sclear hidden" id="q-clear" aria-label="Effacer la recherche">✕</button> |
| 542 | − <select id="period"> |
| |
| 543 | − <option value="">À venir (tout)</option> |
| |
| 544 | − <option value="today">Aujourd'hui</option> |
| |
| 545 | − <option value="weekend">Ce week-end</option> |
| |
| 546 | − <option value="7">7 prochains jours</option> |
| |
| 547 | − <option value="30">30 prochains jours</option> |
| |
| 548 | − </select> |
| |
| 549 | − </label> |
| |
| 550 | − </div> |
| |
| 551 | − <div class="chips" id="cats"></div> |
| |
| 552 | </div> |
799 | </div> |
| |
800 | + <div class="quick" id="quick"></div> |
| |
801 | + <div class="days" id="days" aria-label="Naviguer par jour"></div> |
| |
802 | + </section> |
| 553 | |
803 | |
| 554 | − <div class="meta-line"><span id="count">Chargement…</span></div> |
804 | + <!-- accueil éditorial : sections composées par renderHome() --> |
| |
805 | + <section class="container" id="home"></section> |
| |
806 | + |
| |
807 | + <!-- résultats de recherche / filtres : liste ou carte --> |
| |
808 | + <section class="container hidden" id="results"> |
| |
809 | + <div class="rtool"> |
| |
810 | + <div class="meta-line"><span id="count">Chargement…</span></div> |
| |
811 | + <div class="vtoggle" role="group" aria-label="Affichage"> |
| |
812 | + <button type="button" id="v-list" class="on" aria-pressed="true"> |
| |
813 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" |
| |
814 | + stroke-linecap="round" aria-hidden="true"> |
| |
815 | + <path d="M4 6h16M4 12h16M4 18h16"/></svg>Liste</button> |
| |
816 | + <button type="button" id="v-map" aria-pressed="false"> |
| |
817 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" |
| |
818 | + stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
| |
819 | + <path d="M12 21s-7-5.5-7-11a7 7 0 0 1 14 0c0 5.5-7 11-7 11z"/> |
| |
820 | + <circle cx="12" cy="10" r="2.6"/></svg>Carte</button> |
| |
821 | + </div> |
| |
822 | + </div> |
| |
823 | + <div class="afilters" id="afilters"></div> |
| 555 | <section class="grid" id="grid"></section> |
824 | <section class="grid" id="grid"></section> |
| 556 | <button class="more btn btn-primary" id="more" hidden>Afficher plus d'événements</button> |
825 | <button class="more btn btn-primary" id="more" hidden>Afficher plus d'événements</button> |
| |
826 | + <div class="mapwrap hidden" id="mapwrap"> |
| |
827 | + <div id="map"></div> |
| |
828 | + <div class="map-load" id="map-load">Chargement de la carte…</div> |
| |
829 | + <span class="map-note hidden" id="map-note"></span> |
| |
830 | + </div> |
| 557 | </section> |
831 | </section> |
| 558 | |
832 | |
| 559 | <section class="sources" id="sources"> |
833 | <section class="sources" id="sources"> |
| 568 | </section> |
842 | </section> |
| 569 | </main> |
843 | </main> |
| 570 | |
844 | |
| |
845 | +<!-- ======== PANNEAU DE FILTRES (bottom sheet mobile / modale desktop) ======== --> |
| |
846 | +<div class="sheet-ov" id="sheet-ov"></div> |
| |
847 | +<div class="sheet" id="sheet" role="dialog" aria-modal="true" aria-label="Filtres"> |
| |
848 | + <div class="sheet-grab" aria-hidden="true"></div> |
| |
849 | + <div class="sheet-head"> |
| |
850 | + <h2>Filtres</h2> |
| |
851 | + <button type="button" class="sheet-x" id="sheet-x" aria-label="Fermer les filtres">✕</button> |
| |
852 | + </div> |
| |
853 | + <div class="sheet-body"> |
| |
854 | + <div class="sheet-sec"> |
| |
855 | + <span class="klabel">Quand</span> |
| |
856 | + <div class="sheet-chips" id="sh-when"></div> |
| |
857 | + <div class="sheet-dates"> |
| |
858 | + <input class="input" type="date" id="sh-from" aria-label="Du"> |
| |
859 | + <span class="klabel">au</span> |
| |
860 | + <input class="input" type="date" id="sh-to" aria-label="Au"> |
| |
861 | + </div> |
| |
862 | + </div> |
| |
863 | + <div class="sheet-sec"> |
| |
864 | + <span class="klabel">Région</span> |
| |
865 | + <select class="select" id="region"><option value="">Toutes les régions</option></select> |
| |
866 | + </div> |
| |
867 | + <div class="sheet-sec"> |
| |
868 | + <span class="klabel">Catégories</span> |
| |
869 | + <div class="sheet-chips" id="cats"></div> |
| |
870 | + </div> |
| |
871 | + <div class="sheet-sec"> |
| |
872 | + <span class="klabel">Prix</span> |
| |
873 | + <div class="sheet-chips"> |
| |
874 | + <button type="button" class="chip free" id="chip-free">✦ Gratuit seulement</button> |
| |
875 | + </div> |
| |
876 | + </div> |
| |
877 | + </div> |
| |
878 | + <div class="sheet-foot"> |
| |
879 | + <button type="button" class="btn btn-ghost" id="sh-reset">Réinitialiser</button> |
| |
880 | + <button type="button" class="btn btn-cta" id="sh-apply">Voir les sorties</button> |
| |
881 | + </div> |
| |
882 | +</div> |
| |
883 | + |
| |
884 | +<div class="toast" id="toast" role="status" aria-live="polite"></div> |
| |
885 | + |
| 571 | <!-- ======== VUE FICHE ÉVÉNEMENT ======== --> |
886 | <!-- ======== VUE FICHE ÉVÉNEMENT ======== --> |
| 572 | <main id="view-detail" class="hidden"> |
887 | <main id="view-detail" class="hidden"> |
| 573 | <div class="container detail"> |
888 | <div class="container detail"> |
| 574 | <span class="back" id="back">← Toutes les sorties</span> |
889 | <span class="back" id="back">← Toutes les sorties</span> |
| 575 | <div class="detail-grid"> |
890 | <div class="detail-grid"> |
| 576 | − <article class="gk-card detail-hero"> |
891 | + <article class="detail-main"> |
| 577 | − <div class="img" id="d-img"></div> |
892 | + <div class="d-imgwrap" id="d-img"></div> |
| 578 | − <div class="body"> |
893 | + <div class="d-body"> |
| 579 | <span class="klabel" id="d-cats-label"></span> |
894 | <span class="klabel" id="d-cats-label"></span> |
| 580 | <div id="d-status"></div> |
895 | <div id="d-status"></div> |
| 581 | <div class="d-title-row"><h1 id="d-title"></h1><span id="d-fav"></span></div> |
896 | <div class="d-title-row"><h1 id="d-title"></h1><span id="d-fav"></span></div> |
| 584 | </div> |
899 | </div> |
| 585 | </article> |
900 | </article> |
| 586 | <aside class="side"> |
901 | <aside class="side"> |
| 587 | − <div class="gk-card box" id="d-facts"></div> |
902 | + <div class="box" id="d-facts"></div> |
| 588 | − <a class="btn btn-primary" id="d-cta" target="_blank" rel="noopener">Billets · fiche originale ↗</a> |
903 | + <a class="btn btn-cta" id="d-cta" target="_blank" rel="noopener">Billets · fiche originale ↗</a> |
| 589 | − <div class="gk-card box" id="d-src"></div> |
904 | + <div class="box" id="d-src"></div> |
| 590 | </aside> |
905 | </aside> |
| 591 | </div> |
906 | </div> |
| 592 | <section class="similar"> |
907 | <section class="similar"> |
| 595 | <div class="grid" id="d-similar"></div> |
910 | <div class="grid" id="d-similar"></div> |
| 596 | </section> |
911 | </section> |
| 597 | </div> |
912 | </div> |
| |
913 | + <!-- CTA sticky mobile : le billet toujours à portée de pouce --> |
| |
914 | + <div class="d-ctabar" id="d-ctabar"></div> |
| 598 | </main> |
915 | </main> |
| 599 | |
916 | |
| 600 | <!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== --> |
917 | <!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== --> |
| 762 | /* Sorti-Ka — frontend + routeur. Auteur : Simon-Pierre Boucher <contact@spboucher.ai> */ |
1079 | /* Sorti-Ka — frontend + routeur. Auteur : Simon-Pierre Boucher <contact@spboucher.ai> */ |
| 763 | "use strict"; |
1080 | "use strict"; |
| 764 | const $ = id => document.getElementById(id); |
1081 | const $ = id => document.getElementById(id); |
| 765 | −const state = { q:"", region:"", category:"", free:false, from:"", to:"", offset:0, limit:30, total:0 }; |
1082 | +const state = { q:"", region:"", category:"", free:false, from:"", to:"", near:"", |
| |
1083 | + view:"list", offset:0, limit:30, total:0 }; |
| 766 | |
1084 | |
| 767 | const CAT_LABELS = { |
1085 | const CAT_LABELS = { |
| 768 | "festival":"Festivals","musique":"Musique","arts-scene":"Arts de la scène", |
1086 | "festival":"Festivals","musique":"Musique","arts-scene":"Arts de la scène", |
| 814 | if(!ev.status || !STATUS_LABELS[ev.status]) return ""; |
1132 | if(!ev.status || !STATUS_LABELS[ev.status]) return ""; |
| 815 | return `<span class="badge-status badge-status--${esc(ev.status)}">${STATUS_LABELS[ev.status]}</span>`; |
1133 | return `<span class="badge-status badge-status--${esc(ev.status)}">${STATUS_LABELS[ev.status]}</span>`; |
| 816 | } |
1134 | } |
| 817 | −function priceBadge(ev){ |
1135 | +function money(v){ |
| 818 | − if(ev.is_free === true) return `<span class="badge-free">Gratuit</span>`; |
1136 | + return `${v.toFixed(2).replace(".",",").replace(/,00$/,"")} $`; |
| 819 | − if(ev.price_min != null) return `<span class="badge-price">Dès ${ev.price_min.toFixed(2).replace(".",",")} $</span>`; |
1137 | +} |
| 820 | − if(ev.is_free === false) return `<span class="badge-price">Payant</span>`; |
1138 | +function priceTxt(ev){ |
| |
1139 | + if(ev.is_free === true) return "Gratuit"; |
| |
1140 | + if(ev.price_min != null) return `Dès ${money(ev.price_min)}`; |
| |
1141 | + return ""; |
| |
1142 | +} |
| |
1143 | +const WDAYS = ["dim","lun","mar","mer","jeu","ven","sam"]; |
| |
1144 | +/* date éditoriale des fiches : « Mer 27 août · 20 h » / « 27 août → 3 sept » */ |
| |
1145 | +function evDate(ev){ |
| |
1146 | + if(!ev.start_date) return "Date à confirmer"; |
| |
1147 | + const yNow = new Date().getFullYear(); |
| |
1148 | + const [y,m,d] = ev.start_date.split("-").map(Number); |
| |
1149 | + if(ev.end_date && ev.end_date !== ev.start_date){ |
| |
1150 | + const [y2,m2,d2] = ev.end_date.split("-").map(Number); |
| |
1151 | + return `${d} ${MONTHS[m-1]} → ${d2} ${MONTHS[m2-1]}${y2!==yNow?` ${y2}`:""}`; |
| |
1152 | + } |
| |
1153 | + const wd = WDAYS[new Date(y, m-1, d).getDay()]; |
| |
1154 | + let s = `${wd} ${d} ${MONTHS[m-1]}${y!==yNow?` ${y}`:""}`; |
| |
1155 | + if(ev.start_time && ev.start_time !== "00:00") s += ` · ${fmtTime(ev.start_time)}`; |
| |
1156 | + return s; |
| |
1157 | +} |
| |
1158 | +/* étiquette posée sur l'image : statut sinon prix */ |
| |
1159 | +function evTag(ev){ |
| |
1160 | + if(ev.status && STATUS_LABELS[ev.status]) |
| |
1161 | + return `<span class="ev-tag stat-${esc(ev.status)}">${STATUS_LABELS[ev.status]}</span>`; |
| |
1162 | + if(ev.is_free === true) return `<span class="ev-tag free">Gratuit</span>`; |
| |
1163 | + if(ev.price_min != null) return `<span class="ev-tag">Dès ${money(ev.price_min)}</span>`; |
| 821 | return ""; |
1164 | return ""; |
| 822 | } |
1165 | } |
| |
1166 | +function evImg(ev){ |
| |
1167 | + return ev.image |
| |
1168 | + ? `<img src="${esc(ev.image)}" alt="" loading="lazy" |
| |
1169 | + onerror="this.parentElement.classList.add('noimg');this.remove()">` : ""; |
| |
1170 | +} |
| 823 | |
1171 | |
| |
1172 | +/* fiche standard (grilles + carrousels) — l'image domine, hiérarchie |
| |
1173 | + DATE / titre / lieu · ville / prix (sur l'image) / catégorie · source */ |
| 824 | function card(ev){ |
1174 | function card(ev){ |
| 825 | − const where = [ev.venue, ev.city, ev.region].filter(Boolean) |
1175 | + const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · "); |
| 826 | − .map((x,i)=> i===1?`<b>${esc(x)}</b>`:esc(x)).join(" · "); |
1176 | + const cat = (ev.categories||[]).filter(c=>c!=="autre")[0]; |
| 827 | − const cats = (ev.categories||[]).filter(c=>c!=="autre").slice(0,3) |
1177 | + return `<a class="ev" data-nav href="/evenement/${esc(ev.uid)}"> |
| 828 | − .map(c=>`<span class="cat">${esc(CAT_LABELS[c]||c)}</span>`).join(""); |
1178 | + <div class="ev-img${ev.image?"":" noimg"}">${evImg(ev)}${evTag(ev)}${favBtn(ev)}</div> |
| 829 | − const img = ev.image |
1179 | + <div class="ev-body"> |
| 830 | − ? `<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">` : ""; |
1180 | + <span class="ev-date">${evDate(ev)}</span> |
| 831 | − return `<a class="card gk-card gk-hover" data-nav href="/evenement/${esc(ev.uid)}"> |
1181 | + <h3>${esc(ev.title)}</h3> |
| 832 | − <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span>${favBtn(ev)}</div> |
1182 | + <p class="ev-where">${where || esc(ev.region || "Québec")}</p> |
| 833 | − <div class="card-body"> |
1183 | + <p class="ev-meta">${cat?`<span>${esc(CAT_LABELS[cat]||cat)}</span>`:""} |
| 834 | − <div class="card-top"><h3>${esc(ev.title)}</h3>${statusBadge(ev)||priceBadge(ev)}</div> |
1184 | + <span class="src">${esc(SRC_LABELS[ev.source]||ev.source)}</span></p> |
| 835 | − ${ev.description?`<div class="desc">${esc(ev.description)}</div>`:""} |
1185 | + </div></a>`; |
| 836 | − <div class="where">${where||"Lieu à confirmer"}</div> |
1186 | +} |
| 837 | − <div class="cats">${cats}</div> |
1187 | + |
| 838 | − <div class="src"><span>${SRC_LABELS[ev.source]||ev.source}</span><span class="go">Fiche →</span></div> |
1188 | +/* grand événement vedette — image pleine largeur, texte en surimpression */ |
| 839 | − </div> |
1189 | +function featCard(ev){ |
| 840 | − </a>`; |
1190 | + const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · "); |
| |
1191 | + const p = priceTxt(ev); |
| |
1192 | + return `<a class="feat" data-nav href="/evenement/${esc(ev.uid)}"> |
| |
1193 | + ${evImg(ev)}<span class="feat-kick">À la une</span>${favBtn(ev)} |
| |
1194 | + <div class="feat-body"> |
| |
1195 | + <span class="feat-date">${evDate(ev)}</span> |
| |
1196 | + <h3>${esc(ev.title)}</h3> |
| |
1197 | + <p class="feat-where">${where}${p?` — <b>${p}</b>`:""}</p> |
| |
1198 | + </div></a>`; |
| 841 | } |
1199 | } |
| 842 | |
1200 | |
| |
1201 | +/* rangée compacte (À découvrir) */ |
| |
1202 | +function compactCard(ev){ |
| |
1203 | + const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · "); |
| |
1204 | + return `<a class="evc" data-nav href="/evenement/${esc(ev.uid)}"> |
| |
1205 | + <span class="evc-img${ev.image?"":" noimg"}">${evImg(ev)}</span> |
| |
1206 | + <span class="evc-body"> |
| |
1207 | + <span class="ev-date">${evDate(ev)}</span> |
| |
1208 | + <h3>${esc(ev.title)}</h3> |
| |
1209 | + <p class="ev-where">${where || esc(ev.region || "Québec")}</p> |
| |
1210 | + </span></a>`; |
| |
1211 | +} |
| |
1212 | + |
| |
1213 | +/* ---------- état des filtres ---------- */ |
| |
1214 | +function isoLocal(d){ |
| |
1215 | + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; |
| |
1216 | +} |
| |
1217 | +const todayISO = () => isoLocal(new Date()); |
| |
1218 | +/* fin de semaine : vendredi → dimanche (ou aujourd'hui si déjà entamée) */ |
| |
1219 | +function weekendRange(){ |
| |
1220 | + const t = new Date(), day = t.getDay(); |
| |
1221 | + const fri = new Date(t); |
| |
1222 | + if(day >= 1 && day <= 4) fri.setDate(t.getDate() + (5 - day)); |
| |
1223 | + const sun = new Date(fri); |
| |
1224 | + sun.setDate(fri.getDate() + (fri.getDay() === 0 ? 0 : 7 - fri.getDay())); |
| |
1225 | + return [isoLocal(fri), isoLocal(sun)]; |
| |
1226 | +} |
| |
1227 | +function plusDays(n){ |
| |
1228 | + const d = new Date(); d.setDate(d.getDate()+n); return isoLocal(d); |
| |
1229 | +} |
| |
1230 | +function isFiltering(){ |
| |
1231 | + return !!(state.q || state.region || state.category || state.free |
| |
1232 | + || state.from || state.to || state.near); |
| |
1233 | +} |
| 843 | function params(){ |
1234 | function params(){ |
| 844 | const p = new URLSearchParams(); |
1235 | const p = new URLSearchParams(); |
| 845 | if(state.q) p.set("q", state.q); |
1236 | if(state.q) p.set("q", state.q); |
| 848 | if(state.free) p.set("free","true"); |
1239 | if(state.free) p.set("free","true"); |
| 849 | if(state.from) p.set("from", state.from); |
1240 | if(state.from) p.set("from", state.from); |
| 850 | if(state.to) p.set("to", state.to); |
1241 | if(state.to) p.set("to", state.to); |
| |
1242 | + if(state.near){ p.set("near", state.near); p.set("radius_km","30"); p.set("sort","near"); } |
| 851 | p.set("limit", state.limit); p.set("offset", state.offset); |
1243 | p.set("limit", state.limit); p.set("offset", state.offset); |
| 852 | return p.toString(); |
1244 | return p.toString(); |
| 853 | } |
1245 | } |
| |
1246 | +async function fetchEvents(qs){ |
| |
1247 | + return await (await fetch(`/api/events?${qs}`)).json(); |
| |
1248 | +} |
| |
1249 | + |
| |
1250 | +/* ---------- accueil éditorial : sections composées ---------- */ |
| |
1251 | +let HOME_DONE = false; |
| |
1252 | +const HOME_DEFS = [ |
| |
1253 | + {id:"alaune", sk:"Sélection Sorti·Ka", t:"À la une", mode:"feat", |
| |
1254 | + q:{limit:30}}, |
| |
1255 | + {id:"cesoir", sk:"Aujourd'hui", t:"Ce soir", mode:"row", |
| |
1256 | + q:()=>({from:todayISO(), to:todayISO(), limit:24}), all:()=>({from:todayISO(), to:todayISO()})}, |
| |
1257 | + {id:"weekend", sk:"Vendredi → dimanche", t:"Cette fin de semaine", mode:"grid", |
| |
1258 | + q:()=>{ const [a,b]=weekendRange(); return {from:a, to:b, limit:8}; }, |
| |
1259 | + all:()=>{ const [a,b]=weekendRange(); return {from:a, to:b}; }}, |
| |
1260 | + {id:"gratuit", sk:"0 $", t:"Gratuit", mode:"row", |
| |
1261 | + q:{free:true, limit:14}, all:{free:true}}, |
| |
1262 | + {id:"musique", sk:"Catégorie", t:"Concerts & musique", mode:"row", |
| |
1263 | + q:{category:"musique", limit:14}, all:{category:"musique"}}, |
| |
1264 | + {id:"festival", sk:"Catégorie", t:"Festivals", mode:"row", |
| |
1265 | + q:{category:"festival", limit:14}, all:{category:"festival"}}, |
| |
1266 | + {id:"famille", sk:"Catégorie", t:"Famille", mode:"row", |
| |
1267 | + q:{category:"famille", limit:14}, all:{category:"famille"}}, |
| |
1268 | + {id:"decouvrir", sk:"Fraîchement ajoutés", t:"À découvrir", mode:"compact", |
| |
1269 | + q:{sort:"recent", limit:10}}, |
| |
1270 | +]; |
| |
1271 | +function secHead(d, total){ |
| |
1272 | + const n = total ? `<button type="button" class="sec-all" data-all="${d.id}"> |
| |
1273 | + Tout voir (${total.toLocaleString("fr-CA")}) →</button>` : ""; |
| |
1274 | + return `<div class="sec-head"><div><span class="sk">${d.sk}</span><h2>${d.t}</h2></div>${n}</div>`; |
| |
1275 | +} |
| |
1276 | +function heroMosaic(evs){ |
| |
1277 | + const tiles = evs.filter(e=>e.image).slice(0,12); |
| |
1278 | + if(!tiles.length) return; |
| |
1279 | + $("hero-mosaic").innerHTML = tiles.map(e => |
| |
1280 | + `<span class="tile"><img src="${esc(e.image)}" alt="" loading="lazy" |
| |
1281 | + onerror="this.remove()"></span>`).join(""); |
| |
1282 | +} |
| |
1283 | +/* durée d'un événement en jours (0 = un seul jour) */ |
| |
1284 | +function evSpan(e){ |
| |
1285 | + if(!e.end_date || !e.start_date || e.end_date === e.start_date) return 0; |
| |
1286 | + return (new Date(e.end_date) - new Date(e.start_date)) / 864e5; |
| |
1287 | +} |
| |
1288 | +async function renderHome(){ |
| |
1289 | + if(HOME_DONE) return; |
| |
1290 | + HOME_DONE = true; |
| |
1291 | + const host = $("home"); |
| |
1292 | + host.innerHTML = HOME_DEFS.map(d=>`<section class="sec" id="sec-${d.id}"></section>`).join(""); |
| |
1293 | + const results = await Promise.all(HOME_DEFS.map(async d => { |
| |
1294 | + try{ |
| |
1295 | + const spec = typeof d.q === "function" ? d.q() : d.q; |
| |
1296 | + const p = new URLSearchParams(); |
| |
1297 | + Object.entries(spec).forEach(([k,v]) => p.set(k, v)); |
| |
1298 | + return [d, await fetchEvents(p.toString())]; |
| |
1299 | + }catch(e){ return [d, null]; } |
| |
1300 | + })); |
| |
1301 | + // rendu séquentiel avec dédoublonnage inter-sections : un événement déjà mis |
| |
1302 | + // en vitrine plus haut ne se répète pas ; les expos/festivals au long cours |
| |
1303 | + // cèdent la vedette aux événements datés (le rythme vient de la variété) |
| |
1304 | + const seen = new Set(); |
| |
1305 | + const take = (evs, n) => { |
| |
1306 | + const out = evs.slice(0, n); |
| |
1307 | + out.forEach(e => seen.add(e.uid)); |
| |
1308 | + return out; |
| |
1309 | + }; |
| |
1310 | + for(const [d, data] of results){ |
| |
1311 | + const el = $(`sec-${d.id}`); |
| |
1312 | + if(!el) continue; |
| |
1313 | + if(!data){ el.remove(); continue; } |
| |
1314 | + let evs = (data.events || []).filter(e => !seen.has(e.uid)); |
| |
1315 | + if(d.id === "alaune"){ |
| |
1316 | + evs = evs.filter(e => e.image); |
| |
1317 | + evs.sort((a,b) => (evSpan(a) > 3) - (evSpan(b) > 3)); |
| |
1318 | + } else if(d.id === "cesoir" || d.id === "weekend"){ |
| |
1319 | + evs.sort((a,b) => (evSpan(a) > 3) - (evSpan(b) > 3)); |
| |
1320 | + } |
| |
1321 | + if(!evs.length){ el.remove(); continue; } |
| |
1322 | + if(d.mode === "feat"){ |
| |
1323 | + const picked = take(evs, 10); |
| |
1324 | + el.innerHTML = secHead(d, 0) + featCard(picked[0]) |
| |
1325 | + + (picked.length > 1 ? `<div class="hscroll">${picked.slice(1).map(card).join("")}</div>` : ""); |
| |
1326 | + heroMosaic(evs); |
| |
1327 | + } else if(d.mode === "row"){ |
| |
1328 | + el.innerHTML = secHead(d, data.total) + `<div class="hscroll">${take(evs, 14).map(card).join("")}</div>`; |
| |
1329 | + } else if(d.mode === "grid"){ |
| |
1330 | + el.innerHTML = secHead(d, data.total) + `<div class="grid">${take(evs, 8).map(card).join("")}</div>`; |
| |
1331 | + } else { |
| |
1332 | + el.innerHTML = secHead(d, 0) + `<div class="compact">${take(evs, 10).map(compactCard).join("")}</div>`; |
| |
1333 | + } |
| |
1334 | + bindNav(el); |
| |
1335 | + const btn = el.querySelector("[data-all]"); |
| |
1336 | + if(btn) btn.onclick = () => { |
| |
1337 | + const f = typeof d.all === "function" ? d.all() : (d.all || {}); |
| |
1338 | + resetFilters(false); |
| |
1339 | + if(f.from){ state.from = f.from; state.to = f.to || ""; } |
| |
1340 | + if(f.free) state.free = true; |
| |
1341 | + if(f.category) state.category = f.category; |
| |
1342 | + load(); |
| |
1343 | + window.scrollTo({top: $("results").offsetTop - 130, behavior:"smooth"}); |
| |
1344 | + }; |
| |
1345 | + } |
| |
1346 | + favPaint(); |
| |
1347 | +} |
| |
1348 | + |
| |
1349 | +/* ---------- pilules de filtres actifs (résultats) ---------- */ |
| |
1350 | +function filterPills(){ |
| |
1351 | + const pills = []; |
| |
1352 | + if(state.q) pills.push(["q", `« ${state.q} »`]); |
| |
1353 | + if(state.from && state.from === state.to && state.from === todayISO()) pills.push(["dates","Aujourd'hui"]); |
| |
1354 | + else if(state.from || state.to){ |
| |
1355 | + const w = weekendRange(); |
| |
1356 | + pills.push(["dates", state.from===w[0]&&state.to===w[1] ? "Cette fin de semaine" |
| |
1357 | + : [state.from&&fmtDate(state.from), state.to&&fmtDate(state.to)].filter(Boolean).join(" → ")]); |
| |
1358 | + } |
| |
1359 | + if(state.region) pills.push(["region", state.region]); |
| |
1360 | + if(state.category) pills.push(["category", CAT_LABELS[state.category]||state.category]); |
| |
1361 | + if(state.free) pills.push(["free", "Gratuit"]); |
| |
1362 | + if(state.near) pills.push(["near", "Près de moi"]); |
| |
1363 | + $("afilters").innerHTML = pills.map(([k,label]) => |
| |
1364 | + `<button type="button" class="apill" data-clear="${k}">${esc(label)}<i>✕</i></button>`).join(""); |
| |
1365 | + $("afilters").querySelectorAll("[data-clear]").forEach(b => b.onclick = () => { |
| |
1366 | + const k = b.dataset.clear; |
| |
1367 | + if(k==="q"){ state.q=""; $("q").value=""; } |
| |
1368 | + else if(k==="dates"){ state.from=""; state.to=""; } |
| |
1369 | + else if(k==="region") state.region=""; |
| |
1370 | + else if(k==="category") state.category=""; |
| |
1371 | + else if(k==="free") state.free=false; |
| |
1372 | + else if(k==="near") state.near=""; |
| |
1373 | + load(); |
| |
1374 | + }); |
| |
1375 | +} |
| 854 | |
1376 | |
| |
1377 | +/* ---------- chargement : accueil éditorial OU résultats ---------- */ |
| 855 | async function load(reset=true){ |
1378 | async function load(reset=true){ |
| 856 | − if(reset){ state.offset = 0; } |
1379 | + const filtering = isFiltering(); |
| 857 | − const data = await (await fetch(`/api/events?${params()}`)).json(); |
1380 | + $("home").classList.toggle("hidden", filtering); |
| |
1381 | + $("results").classList.toggle("hidden", !filtering); |
| |
1382 | + syncControls(); |
| |
1383 | + if(!filtering){ renderHome(); return; } |
| |
1384 | + if(reset) state.offset = 0; |
| |
1385 | + filterPills(); |
| |
1386 | + let data; |
| |
1387 | + try{ data = await fetchEvents(params()); } |
| |
1388 | + catch(e){ $("count").textContent = "Erreur de chargement — réessayez."; return; } |
| 858 | state.total = data.total; |
1389 | state.total = data.total; |
| 859 | const html = data.events.map(card).join(""); |
1390 | const html = data.events.map(card).join(""); |
| 860 | if(reset){ |
1391 | if(reset){ |
| 863 | } else { |
1394 | } else { |
| 864 | $("grid").insertAdjacentHTML("beforeend", html); |
1395 | $("grid").insertAdjacentHTML("beforeend", html); |
| 865 | } |
1396 | } |
| 866 | − $("count").innerHTML = `<b>${data.total.toLocaleString("fr-CA")}</b> événement${data.total>1?"s":""} à venir`; |
1397 | + $("count").innerHTML = `<b>${data.total.toLocaleString("fr-CA")}</b> sortie${data.total>1?"s":""} trouvée${data.total>1?"s":""}`; |
| 867 | − $("more").hidden = state.offset + data.events.length >= data.total; |
1398 | + $("sh-apply").textContent = `Voir ${data.total.toLocaleString("fr-CA")} sortie${data.total>1?"s":""}`; |
| |
1399 | + $("more").hidden = state.view === "map" || state.offset + data.events.length >= data.total; |
| 868 | bindNav($("grid")); |
1400 | bindNav($("grid")); |
| |
1401 | + favPaint(); |
| |
1402 | + if(state.view === "map") refreshMarkers(); |
| |
1403 | +} |
| |
1404 | +let loadT; |
| |
1405 | +function loadDebounced(){ clearTimeout(loadT); loadT = setTimeout(load, 220); } |
| |
1406 | + |
| |
1407 | +/* ---------- accès rapides + timeline de jours ---------- */ |
| |
1408 | +const QUICK_DEFS = [ |
| |
1409 | + ["cesoir","Ce soir"], ["weekend","Cette fin de semaine"], ["free","Gratuit"], |
| |
1410 | +]; |
| |
1411 | +const PIN_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" |
| |
1412 | + stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
| |
1413 | + <path d="M12 21s-7-5.5-7-11a7 7 0 0 1 14 0c0 5.5-7 11-7 11z"/><circle cx="12" cy="10" r="2.6"/></svg>`; |
| |
1414 | +const SLIDERS_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" |
| |
1415 | + stroke-linecap="round" aria-hidden="true"> |
| |
1416 | + <path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2.2"/><circle cx="9" cy="17" r="2.2"/></svg>`; |
| |
1417 | +function renderQuick(){ |
| |
1418 | + $("quick").innerHTML = |
| |
1419 | + QUICK_DEFS.map(([k,l]) => `<button type="button" class="qchip" data-qk="${k}">${l}</button>`).join("") |
| |
1420 | + + `<button type="button" class="qchip" data-qk="near">${PIN_SVG}Près de moi</button>` |
| |
1421 | + + `<button type="button" class="qchip" id="btn-filters">${SLIDERS_SVG}Filtres |
| |
1422 | + <span class="fb hidden" id="fcount"></span></button>`; |
| |
1423 | + $("quick").querySelectorAll("[data-qk]").forEach(b => b.onclick = () => { |
| |
1424 | + const k = b.dataset.qk; |
| |
1425 | + if(k === "cesoir"){ |
| |
1426 | + const on = state.from === todayISO() && state.to === todayISO(); |
| |
1427 | + state.from = on ? "" : todayISO(); state.to = on ? "" : todayISO(); |
| |
1428 | + } else if(k === "weekend"){ |
| |
1429 | + const [a,z] = weekendRange(); |
| |
1430 | + const on = state.from === a && state.to === z; |
| |
1431 | + state.from = on ? "" : a; state.to = on ? "" : z; |
| |
1432 | + } else if(k === "free"){ |
| |
1433 | + state.free = !state.free; |
| |
1434 | + } else if(k === "near"){ |
| |
1435 | + toggleNear(); return; |
| |
1436 | + } |
| |
1437 | + load(); |
| |
1438 | + }); |
| |
1439 | + $("btn-filters").onclick = openSheet; |
| |
1440 | +} |
| |
1441 | +function renderDays(){ |
| |
1442 | + const t = new Date(); |
| |
1443 | + let html = `<button type="button" class="day all" data-d=""><small>·Ka</small><b>Tout</b><i> </i></button>`; |
| |
1444 | + for(let i=0; i<15; i++){ |
| |
1445 | + const d = new Date(t); d.setDate(t.getDate()+i); |
| |
1446 | + const iso = isoLocal(d); |
| |
1447 | + const lab = i===0 ? "auj" : i===1 ? "dem" : WDAYS[d.getDay()]; |
| |
1448 | + html += `<button type="button" class="day" data-d="${iso}"> |
| |
1449 | + <small>${lab}</small><b>${d.getDate()}</b><i>${MONTHS[d.getMonth()]}</i></button>`; |
| |
1450 | + } |
| |
1451 | + $("days").innerHTML = html; |
| |
1452 | + $("days").querySelectorAll(".day").forEach(b => b.onclick = () => { |
| |
1453 | + const d = b.dataset.d; |
| |
1454 | + if(!d){ state.from = ""; state.to = ""; } |
| |
1455 | + else if(state.from === d && state.to === d){ state.from = ""; state.to = ""; } |
| |
1456 | + else { state.from = d; state.to = d; } |
| |
1457 | + load(); |
| |
1458 | + }); |
| |
1459 | +} |
| |
1460 | +/* reflète l'état dans les accès rapides, la timeline et le panneau */ |
| |
1461 | +function syncControls(){ |
| |
1462 | + const today = todayISO(), [wa,wz] = weekendRange(); |
| |
1463 | + document.querySelectorAll("[data-qk]").forEach(b => { |
| |
1464 | + const k = b.dataset.qk; |
| |
1465 | + b.classList.toggle("on", |
| |
1466 | + k==="cesoir" ? (state.from===today && state.to===today) |
| |
1467 | + : k==="weekend" ? (state.from===wa && state.to===wz) |
| |
1468 | + : k==="free" ? state.free |
| |
1469 | + : k==="near" ? !!state.near : false); |
| |
1470 | + }); |
| |
1471 | + document.querySelectorAll(".day[data-d]").forEach(b => |
| |
1472 | + b.classList.toggle("on", b.dataset.d ? (state.from===b.dataset.d && state.to===b.dataset.d) |
| |
1473 | + : !state.from && !state.to)); |
| |
1474 | + const n = ["q","region","category","from","near"].filter(k=>state[k]).length + (state.free?1:0); |
| |
1475 | + const fc = $("fcount"); |
| |
1476 | + if(fc){ fc.textContent = n; fc.classList.toggle("hidden", !n); } |
| |
1477 | + $("q-clear").classList.toggle("hidden", !state.q); |
| |
1478 | + // panneau de filtres |
| |
1479 | + if($("region").value !== state.region) $("region").value = state.region; |
| |
1480 | + document.querySelectorAll("#cats .chip[data-cat]").forEach(x => |
| |
1481 | + x.classList.toggle("on", x.dataset.cat === state.category)); |
| |
1482 | + $("chip-free").classList.toggle("on", state.free); |
| |
1483 | + $("sh-from").value = state.from; $("sh-to").value = state.to; |
| |
1484 | + const whenKey = (!state.from && !state.to) ? "" : |
| |
1485 | + (state.from===today && state.to===today) ? "today" : |
| |
1486 | + (state.from===wa && state.to===wz) ? "weekend" : |
| |
1487 | + (state.from===today && state.to===plusDays(7)) ? "7" : |
| |
1488 | + (state.from===today && state.to===plusDays(30)) ? "30" : "custom"; |
| |
1489 | + document.querySelectorAll("#sh-when .chip").forEach(c => |
| |
1490 | + c.classList.toggle("on", c.dataset.w === whenKey)); |
| |
1491 | +} |
| |
1492 | +function resetFilters(reload=true){ |
| |
1493 | + state.region=""; state.category=""; state.free=false; |
| |
1494 | + state.from=""; state.to=""; state.near=""; |
| |
1495 | + if(reload) load(); |
| |
1496 | +} |
| |
1497 | + |
| |
1498 | +/* ---------- géolocalisation « près de moi » ---------- */ |
| |
1499 | +function toast(msg, ms=2600){ |
| |
1500 | + const t = $("toast"); |
| |
1501 | + t.textContent = msg; |
| |
1502 | + t.classList.add("show"); |
| |
1503 | + clearTimeout(t._t); |
| |
1504 | + t._t = setTimeout(()=>t.classList.remove("show"), ms); |
| |
1505 | +} |
| |
1506 | +function toggleNear(){ |
| |
1507 | + if(state.near){ state.near = ""; load(); return; } |
| |
1508 | + if(!navigator.geolocation){ toast("Géolocalisation non disponible sur cet appareil"); return; } |
| |
1509 | + toast("Recherche de votre position…"); |
| |
1510 | + navigator.geolocation.getCurrentPosition( |
| |
1511 | + pos => { |
| |
1512 | + state.near = `${pos.coords.latitude.toFixed(5)},${pos.coords.longitude.toFixed(5)}`; |
| |
1513 | + load(); |
| |
1514 | + toast("Sorties à moins de 30 km de vous"); |
| |
1515 | + }, |
| |
1516 | + () => toast("Position indisponible — vérifiez les permissions de localisation"), |
| |
1517 | + {timeout: 9000, maximumAge: 600000}); |
| |
1518 | +} |
| |
1519 | + |
| |
1520 | +/* ---------- panneau de filtres (bottom sheet / modale) ---------- */ |
| |
1521 | +const WHEN_DEFS = [["","À venir"],["today","Aujourd'hui"],["weekend","Fin de semaine"], |
| |
1522 | + ["7","7 jours"],["30","30 jours"]]; |
| |
1523 | +function openSheet(){ |
| |
1524 | + $("sh-apply").textContent = isFiltering() && state.total |
| |
1525 | + ? `Voir ${state.total.toLocaleString("fr-CA")} sorties` : "Voir les sorties"; |
| |
1526 | + $("sheet").classList.add("open"); |
| |
1527 | + $("sheet-ov").classList.add("open"); |
| |
1528 | + document.documentElement.classList.add("ka-scroll-lock"); |
| |
1529 | + syncControls(); |
| |
1530 | +} |
| |
1531 | +function closeSheet(){ |
| |
1532 | + $("sheet").classList.remove("open"); |
| |
1533 | + $("sheet-ov").classList.remove("open"); |
| |
1534 | + document.documentElement.classList.remove("ka-scroll-lock"); |
| |
1535 | +} |
| |
1536 | +function initSheet(){ |
| |
1537 | + $("sh-when").innerHTML = WHEN_DEFS.map(([v,l]) => |
| |
1538 | + `<button type="button" class="chip" data-w="${v}">${l}</button>`).join(""); |
| |
1539 | + $("sh-when").querySelectorAll(".chip").forEach(c => c.onclick = () => { |
| |
1540 | + const v = c.dataset.w; |
| |
1541 | + if(v === "today"){ state.from = todayISO(); state.to = todayISO(); } |
| |
1542 | + else if(v === "weekend"){ const [a,z] = weekendRange(); state.from = a; state.to = z; } |
| |
1543 | + else if(v === "7"){ state.from = todayISO(); state.to = plusDays(7); } |
| |
1544 | + else if(v === "30"){ state.from = todayISO(); state.to = plusDays(30); } |
| |
1545 | + else { state.from = ""; state.to = ""; } |
| |
1546 | + loadDebounced(); syncControls(); |
| |
1547 | + }); |
| |
1548 | + const onDate = () => { |
| |
1549 | + state.from = $("sh-from").value; state.to = $("sh-to").value; |
| |
1550 | + loadDebounced(); syncControls(); |
| |
1551 | + }; |
| |
1552 | + $("sh-from").onchange = onDate; $("sh-to").onchange = onDate; |
| |
1553 | + $("region").onchange = e => { state.region = e.target.value; loadDebounced(); }; |
| |
1554 | + $("chip-free").onclick = () => { state.free = !state.free; loadDebounced(); syncControls(); }; |
| |
1555 | + $("sh-reset").onclick = () => { resetFilters(); }; |
| |
1556 | + $("sh-apply").onclick = closeSheet; |
| |
1557 | + $("sheet-x").onclick = closeSheet; |
| |
1558 | + $("sheet-ov").onclick = closeSheet; |
| |
1559 | + document.addEventListener("keydown", e => { if(e.key === "Escape") closeSheet(); }); |
| 869 | } |
1560 | } |
| 870 | |
1561 | |
| 871 | −function setPeriod(v){ |
1562 | +/* ---------- vue carte (Leaflet, chargé à la demande) ---------- */ |
| 872 | − const today = new Date(); const iso = d => d.toISOString().slice(0,10); |
1563 | +const MAPS = {inst:null, layer:null, loading:false}; |
| 873 | − state.from = ""; state.to = ""; |
1564 | +function ensureLeaflet(){ |
| 874 | − if(v==="today"){ state.from = iso(today); state.to = iso(today); } |
1565 | + if(window.L) return Promise.resolve(); |
| 875 | − else if(v==="weekend"){ |
1566 | + return new Promise((res, rej) => { |
| 876 | − const day = today.getDay(); |
1567 | + const c = document.createElement("link"); |
| 877 | − const sat = new Date(today); |
1568 | + c.rel = "stylesheet"; |
| 878 | − if(day !== 6 && day !== 0) sat.setDate(today.getDate() + (6 - day)); |
1569 | + c.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; |
| 879 | − const sun = new Date(sat); sun.setDate(sat.getDate() + (day === 0 ? 0 : 1)); |
1570 | + document.head.appendChild(c); |
| 880 | − state.from = iso(day===6||day===0 ? today : sat); state.to = iso(sun); |
1571 | + const s = document.createElement("script"); |
| |
1572 | + s.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; |
| |
1573 | + s.onload = res; s.onerror = rej; |
| |
1574 | + document.body.appendChild(s); |
| |
1575 | + }); |
| |
1576 | +} |
| |
1577 | +function setView(v){ |
| |
1578 | + state.view = v; |
| |
1579 | + $("v-list").classList.toggle("on", v === "list"); |
| |
1580 | + $("v-list").setAttribute("aria-pressed", v === "list"); |
| |
1581 | + $("v-map").classList.toggle("on", v === "map"); |
| |
1582 | + $("v-map").setAttribute("aria-pressed", v === "map"); |
| |
1583 | + $("grid").classList.toggle("hidden", v === "map"); |
| |
1584 | + $("mapwrap").classList.toggle("hidden", v !== "map"); |
| |
1585 | + $("more").hidden = v === "map" || state.offset + state.limit >= state.total; |
| |
1586 | + if(v === "map") openMap(); |
| |
1587 | +} |
| |
1588 | +async function openMap(){ |
| |
1589 | + try{ await ensureLeaflet(); } |
| |
1590 | + catch(e){ $("map-load").textContent = "Carte indisponible — vérifiez la connexion."; return; } |
| |
1591 | + if(!MAPS.inst){ |
| |
1592 | + MAPS.inst = L.map("map", {zoomControl:true}).setView([46.83,-71.25], 6); |
| |
1593 | + L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", { |
| |
1594 | + attribution:"© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> · © <a href='https://carto.com/attributions'>CARTO</a>", |
| |
1595 | + maxZoom: 19, subdomains:"abcd"}).addTo(MAPS.inst); |
| |
1596 | + MAPS.layer = L.layerGroup().addTo(MAPS.inst); |
| |
1597 | + MAPS.inst.on("popupopen", e => bindNav(e.popup.getElement())); |
| 881 | } |
1598 | } |
| 882 | − else if(v){ const end = new Date(today); end.setDate(today.getDate()+Number(v)); state.from = iso(today); state.to = iso(end); } |
1599 | + $("map-load").classList.add("hidden"); |
| |
1600 | + setTimeout(() => MAPS.inst.invalidateSize(), 80); |
| |
1601 | + refreshMarkers(); |
| |
1602 | +} |
| |
1603 | +function popupHTML(ev){ |
| |
1604 | + return `<a class="mpop" data-nav href="/evenement/${esc(ev.uid)}"> |
| |
1605 | + ${ev.image?`<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">`:""} |
| |
1606 | + <span class="mp-b"><span class="ev-date">${evDate(ev)}</span> |
| |
1607 | + <h4>${esc(ev.title)}</h4> |
| |
1608 | + <p>${[ev.venue, ev.city].filter(Boolean).map(esc).join(" · ")}${priceTxt(ev)?` — ${priceTxt(ev)}`:""}</p> |
| |
1609 | + </span></a>`; |
| |
1610 | +} |
| |
1611 | +/* jusqu'à 600 événements géolocalisés pour les filtres courants */ |
| |
1612 | +async function refreshMarkers(){ |
| |
1613 | + if(!MAPS.inst || MAPS.loading) return; |
| |
1614 | + MAPS.loading = true; |
| |
1615 | + try{ |
| |
1616 | + const evs = []; |
| |
1617 | + for(let page=0; page<3; page++){ |
| |
1618 | + const p = new URLSearchParams(params()); |
| |
1619 | + p.set("limit","200"); p.set("offset", String(page*200)); |
| |
1620 | + const data = await fetchEvents(p.toString()); |
| |
1621 | + evs.push(...data.events); |
| |
1622 | + if((page+1)*200 >= data.total) break; |
| |
1623 | + } |
| |
1624 | + MAPS.layer.clearLayers(); |
| |
1625 | + const pts = []; |
| |
1626 | + evs.filter(e => e.lat && e.lng).forEach(ev => { |
| |
1627 | + pts.push([ev.lat, ev.lng]); |
| |
1628 | + L.circleMarker([ev.lat, ev.lng], {radius:7, color:"#a12551", weight:1.5, |
| |
1629 | + fillColor:"#d6336c", fillOpacity:.85}) |
| |
1630 | + .bindPopup(popupHTML(ev), {closeButton:false}).addTo(MAPS.layer); |
| |
1631 | + }); |
| |
1632 | + if(state.near){ |
| |
1633 | + const [la,ln] = state.near.split(",").map(Number); |
| |
1634 | + L.circleMarker([la,ln], {radius:9, color:"#141814", weight:2.5, |
| |
1635 | + fillColor:"#1c5c41", fillOpacity:1}).bindTooltip("Vous êtes ici").addTo(MAPS.layer); |
| |
1636 | + MAPS.inst.setView([la,ln], 11); |
| |
1637 | + } else if(pts.length){ |
| |
1638 | + MAPS.inst.fitBounds(L.latLngBounds(pts).pad(0.12), {maxZoom: 13}); |
| |
1639 | + } |
| |
1640 | + const note = $("map-note"); |
| |
1641 | + note.textContent = `${pts.length.toLocaleString("fr-CA")} sur ${state.total.toLocaleString("fr-CA")} sorties géolocalisées — affinez avec les filtres`; |
| |
1642 | + note.classList.toggle("hidden", !pts.length); |
| |
1643 | + }catch(e){} |
| |
1644 | + MAPS.loading = false; |
| 883 | } |
1645 | } |
| 884 | |
1646 | |
| 885 | /* ---------- fiche événement ---------- */ |
1647 | /* ---------- fiche événement ---------- */ |
| 892 | document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`; |
1654 | document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`; |
| 893 | $("d-img").innerHTML = ev.image |
1655 | $("d-img").innerHTML = ev.image |
| 894 | ? `<img src="${esc(ev.image)}" alt="" onerror="this.remove()">` : ""; |
1656 | ? `<img src="${esc(ev.image)}" alt="" onerror="this.remove()">` : ""; |
| |
1657 | + $("d-img").classList.toggle("noimg", !ev.image); |
| 895 | $("d-cats-label").textContent = (ev.categories||[]).filter(c=>c!=="autre") |
1658 | $("d-cats-label").textContent = (ev.categories||[]).filter(c=>c!=="autre") |
| 896 | .map(c=>CAT_LABELS[c]||c).join(" · ") || "Événement"; |
1659 | .map(c=>CAT_LABELS[c]||c).join(" · ") || "Événement"; |
| 897 | $("d-status").innerHTML = ev.status && STATUS_LABELS[ev.status] |
1660 | $("d-status").innerHTML = ev.status && STATUS_LABELS[ev.status] |
| 925 | `<div class="fact"><span class="k">${k}</span><span class="v">${v}</span></div>`).join(""); |
1688 | `<div class="fact"><span class="k">${k}</span><span class="v">${v}</span></div>`).join(""); |
| 926 | |
1689 | |
| 927 | $("d-cta").href = ev.url; |
1690 | $("d-cta").href = ev.url; |
| |
1691 | + $("d-ctabar").innerHTML = `<div class="p"> |
| |
1692 | + <b>${priceTxt(ev) || (ev.is_free === false ? "Payant — voir la billetterie" : "Voir la billetterie")}</b> |
| |
1693 | + <span>${evDate(ev)}</span></div> |
| |
1694 | + <a class="btn btn-cta" href="${esc(ev.url)}" target="_blank" rel="noopener">Billets ↗</a>`; |
| 928 | $("d-src").innerHTML = `<span class="klabel">Source & attribution</span> |
1695 | $("d-src").innerHTML = `<span class="klabel">Source & attribution</span> |
| 929 | <div style="margin-top:6px;font-size:13px">${SRC_LABELS[ev.source]||ev.source}</div> |
1696 | <div style="margin-top:6px;font-size:13px">${SRC_LABELS[ev.source]||ev.source}</div> |
| 930 | <div style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3);margin-top:4px"> |
1697 | <div style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3);margin-top:4px"> |
| 1796 | function favCard(it){ |
2563 | function favCard(it){ |
| 1797 | FAV.meta[it.item_id] = it; |
2564 | FAV.meta[it.item_id] = it; |
| 1798 | const img = it.image_url |
2565 | const img = it.image_url |
| 1799 | − ? `<img src="${esc(it.image_url)}" alt="" loading="lazy" onerror="this.remove()">` : ""; |
2566 | + ? `<img src="${esc(it.image_url)}" alt="" loading="lazy" |
| |
2567 | + onerror="this.parentElement.classList.add('noimg');this.remove()">` : ""; |
| 1800 | const local = (it.url || "").replace(/^https?:\/\/[^/]*sorti-ka\.com/i, "") |
2568 | const local = (it.url || "").replace(/^https?:\/\/[^/]*sorti-ka\.com/i, "") |
| 1801 | || `/evenement/${encodeURIComponent(it.item_id)}`; |
2569 | || `/evenement/${encodeURIComponent(it.item_id)}`; |
| 1802 | − return `<a class="card gk-card gk-hover" data-nav href="${esc(local)}"> |
2570 | + return `<a class="ev" data-nav href="${esc(local)}"> |
| 1803 | − <div class="card-img">${img} |
2571 | + <div class="ev-img${it.image_url?"":" noimg"}">${img} |
| |
2572 | + ${it.price_label?`<span class="ev-tag${it.price_label==="Gratuit"?" free":""}">${esc(it.price_label)}</span>`:""} |
| 1804 | <button type="button" class="fav-btn on" data-fav="${esc(it.item_id)}" aria-pressed="true" |
2573 | <button type="button" class="fav-btn on" data-fav="${esc(it.item_id)}" aria-pressed="true" |
| 1805 | aria-label="Retirer de mes favoris (Mon univers Ka)" |
2574 | aria-label="Retirer de mes favoris (Mon univers Ka)" |
| 1806 | title="Retirer de mes favoris (Mon univers Ka)">${FAV_HEART}</button> |
2575 | title="Retirer de mes favoris (Mon univers Ka)">${FAV_HEART}</button> |
| 1807 | </div> |
2576 | </div> |
| 1808 | − <div class="card-body"> |
2577 | + <div class="ev-body"> |
| 1809 | − <div class="card-top"><h3>${esc(it.title)}</h3>${it.price_label?`<span class="badge-price">${esc(it.price_label)}</span>`:""}</div> |
2578 | + <h3>${esc(it.title)}</h3> |
| 1810 | − ${it.subtitle?`<div class="where">${esc(it.subtitle)}</div>`:""} |
2579 | + ${it.subtitle?`<p class="ev-where">${esc(it.subtitle)}</p>`:""} |
| 1811 | − <div class="src"><span>Mon univers Ka</span><span class="go">Fiche →</span></div> |
2580 | + <p class="ev-meta"><span class="src">Mon univers Ka</span></p> |
| 1812 | </div> |
2581 | </div> |
| 1813 | </a>`; |
2582 | </a>`; |
| 1814 | } |
2583 | } |
| 1874 | else if(location.pathname === "/contact") showContact(); |
2643 | else if(location.pathname === "/contact") showContact(); |
| 1875 | else if(location.pathname === "/favoris") showFavoris(); |
2644 | else if(location.pathname === "/favoris") showFavoris(); |
| 1876 | else if(location.pathname === "/stats") showStats(); |
2645 | else if(location.pathname === "/stats") showStats(); |
| 1877 | − else if(r){ |
2646 | + else { |
| 1878 | − // page SSR /region/{slug} : la SPA ouvre la recherche filtrée sur la région |
2647 | + if(r){ |
| 1879 | − const name = allRegions.find(x => kaSlug(x) === decodeURIComponent(r[1])); |
2648 | + // page SSR /region/{slug} : la SPA ouvre la recherche filtrée sur la région |
| 1880 | − if(name){ state.region = name; $("region").value = name; } |
2649 | + const name = allRegions.find(x => kaSlug(x) === decodeURIComponent(r[1])); |
| 1881 | − showList(); load(); |
2650 | + if(name) state.region = name; |
| 1882 | − } |
2651 | + } else if(c){ |
| 1883 | − else if(c){ |
2652 | + // page SSR /categorie/{cat} : recherche filtrée sur la catégorie |
| 1884 | − // page SSR /categorie/{cat} : recherche filtrée sur la catégorie |
2653 | + state.category = decodeURIComponent(c[1]); |
| 1885 | − const cat = decodeURIComponent(c[1]); |
2654 | + } |
| 1886 | − state.category = cat; |
2655 | + showList(); |
| 1887 | − document.querySelectorAll(".chip[data-cat]").forEach(x=>x.classList.toggle("on", x.dataset.cat===cat)); |
2656 | + load(); |
| 1888 | − showList(); load(); |
| |
| 1889 | } |
2657 | } |
| 1890 | − else showList(); |
| |
| 1891 | } |
2658 | } |
| 1892 | function go(path){ |
2659 | function go(path){ |
| 1893 | history.pushState({}, "", path); |
2660 | history.pushState({}, "", path); |
| 1922 | /* ---------- init ---------- */ |
2689 | /* ---------- init ---------- */ |
| 1923 | async function init(){ |
2690 | async function init(){ |
| 1924 | favLoad(); // favoris ♥ — repeint les cœurs dès la réponse du hub |
2691 | favLoad(); // favoris ♥ — repeint les cœurs dès la réponse du hub |
| 1925 | − const regions = await (await fetch("/api/regions")).json(); |
2692 | + renderQuick(); |
| 1926 | − allRegions = regions; |
2693 | + renderDays(); |
| 1927 | − $("region").insertAdjacentHTML("beforeend", |
2694 | + initSheet(); |
| 1928 | − regions.map(r=>`<option>${esc(r)}</option>`).join("")); |
2695 | + |
| 1929 | − |
2696 | + // recherche au centre |
| 1930 | − const cats = await (await fetch("/api/categories")).json(); |
2697 | + let t; $("q").addEventListener("input", e=>{ |
| 1931 | − $("cats").innerHTML = |
2698 | + clearTimeout(t); t=setTimeout(()=>{ state.q=e.target.value.trim(); load(); }, 280); |
| 1932 | − `<span class="chip free" id="chip-free">✦ Gratuit</span>` + |
| |
| 1933 | − cats.filter(c=>c!=="autre").map(c=>`<span class="chip" data-cat="${c}">${CAT_LABELS[c]||c}</span>`).join(""); |
| |
| 1934 | − $("chip-free").onclick = e => { state.free=!state.free; e.target.classList.toggle("on",state.free); load(); }; |
| |
| 1935 | − document.querySelectorAll(".chip[data-cat]").forEach(ch => ch.onclick = () => { |
| |
| 1936 | − const c = ch.dataset.cat; |
| |
| 1937 | − state.category = state.category===c ? "" : c; |
| |
| 1938 | − document.querySelectorAll(".chip[data-cat]").forEach(x=>x.classList.toggle("on", x.dataset.cat===state.category)); |
| |
| 1939 | − load(); |
| |
| 1940 | }); |
2699 | }); |
| |
2700 | + $("q-clear").onclick = () => { $("q").value=""; state.q=""; load(); }; |
| |
2701 | + |
| |
2702 | + // bascule Liste | Carte + pagination |
| |
2703 | + $("v-list").onclick = () => setView("list"); |
| |
2704 | + $("v-map").onclick = () => setView("map"); |
| |
2705 | + $("more").onclick = () => { state.offset += state.limit; load(false); }; |
| |
2706 | + bindNav(document); |
| |
2707 | + |
| |
2708 | + // référentiels (avant route() : /region/{slug} en a besoin) |
| |
2709 | + try{ |
| |
2710 | + const regions = await (await fetch("/api/regions")).json(); |
| |
2711 | + allRegions = regions; |
| |
2712 | + $("region").insertAdjacentHTML("beforeend", |
| |
2713 | + regions.map(r=>`<option>${esc(r)}</option>`).join("")); |
| |
2714 | + }catch(e){} |
| |
2715 | + try{ |
| |
2716 | + const cats = await (await fetch("/api/categories")).json(); |
| |
2717 | + $("cats").innerHTML = cats.filter(c=>c!=="autre") |
| |
2718 | + .map(c=>`<button type="button" class="chip" data-cat="${c}">${CAT_LABELS[c]||c}</button>`).join(""); |
| |
2719 | + document.querySelectorAll("#cats .chip[data-cat]").forEach(ch => ch.onclick = () => { |
| |
2720 | + state.category = state.category===ch.dataset.cat ? "" : ch.dataset.cat; |
| |
2721 | + loadDebounced(); syncControls(); |
| |
2722 | + }); |
| |
2723 | + }catch(e){} |
| 1941 | |
2724 | |
| |
2725 | + // compteurs = preuve de richesse, intégrés au hero en phrase éditoriale |
| 1942 | try{ |
2726 | try{ |
| 1943 | const s = await (await fetch("/api/stats")).json(); |
2727 | const s = await (await fetch("/api/stats")).json(); |
| 1944 | const nReg = Object.keys(s.by_region).filter(r=>r!=="Non rattachée").length; |
2728 | const nReg = Object.keys(s.by_region).filter(r=>r!=="Non rattachée").length; |
| 1945 | − $("stats").innerHTML = [ |
2729 | + const f = n => n.toLocaleString("fr-CA"); |
| 1946 | − `<span class="stat-chip"><span class="pulse"></span><b>${s.upcoming.toLocaleString("fr-CA")}</b> événements à venir</span>`, |
2730 | + $("stats").innerHTML = |
| 1947 | − `<span class="stat-chip"><b>${s.free_upcoming.toLocaleString("fr-CA")}</b> gratuits</span>`, |
2731 | + `<b>${f(s.upcoming)}</b> événements à venir <i>·</i> dans <b>${f(s.cities)}</b> villes ` |
| 1948 | − `<span class="stat-chip"><b>${nReg}</b> régions</span>`, |
2732 | + + `<i>·</i> sur <b>${nReg}</b> régions <i>·</i> dont <b>${f(s.free_upcoming)}</b> gratuits`; |
| 1949 | − `<span class="stat-chip"><b>${s.cities.toLocaleString("fr-CA")}</b> villes</span>`, |
| |
| 1950 | − ].join(""); |
| |
| 1951 | }catch(e){} |
2733 | }catch(e){} |
| 1952 | |
2734 | |
| 1953 | try{ |
2735 | try{ |
| 1954 | const reg = await (await fetch("/api/sources")).json(); |
2736 | const reg = await (await fetch("/api/sources")).json(); |
| 1955 | $("src-grid").innerHTML = (reg.sources||[]).filter(s=>s.statut==="actif"||s.active_events>0).map(s=>` |
2737 | $("src-grid").innerHTML = (reg.sources||[]).filter(s=>s.statut==="actif"||s.active_events>0).map(s=>` |
| 1956 | − <div class="src-card gk-card"> |
2738 | + <div class="src-card"> |
| 1957 | <h4>${esc(s.name)}</h4> |
2739 | <h4>${esc(s.name)}</h4> |
| 1958 | <div class="lic">${esc((s.acces_legal||"").split("—")[0])}</div> |
2740 | <div class="lic">${esc((s.acces_legal||"").split("—")[0])}</div> |
| 1959 | <div class="n"><b>${(s.active_events||0).toLocaleString("fr-CA")}</b> événements actifs · ${esc((s.extraction||"").split("(")[0])}</div> |
2741 | <div class="n"><b>${(s.active_events||0).toLocaleString("fr-CA")}</b> événements actifs · ${esc((s.extraction||"").split("(")[0])}</div> |
| 1978 | }; |
2760 | }; |
| 1979 | } else { |
2761 | } else { |
| 1980 | $("auth").innerHTML = `<a class="btn-ka" href="/api/auth/ka/login"> |
2762 | $("auth").innerHTML = `<a class="btn-ka" href="/api/auth/ka/login"> |
| 1981 | − <span class="kdot">·Ka</span> Se connecter avec KA</a> |
2763 | + <span class="kdot">·Ka</span><span class="tx">Se connecter avec KA</span></a> |
| 1982 | <a class="ka-signup" href="https://www.groupe-ka.com/connexion" |
2764 | <a class="ka-signup" href="https://www.groupe-ka.com/connexion" |
| 1983 | target="_blank" rel="noopener">Créer un compte</a>`; |
2765 | target="_blank" rel="noopener">Créer un compte</a>`; |
| 1984 | } |
2766 | } |
| 1985 | } |
2767 | } |
| 1986 | }catch(e){} |
2768 | }catch(e){} |
| 1987 | |
2769 | |
| 1988 | − let t; $("q").addEventListener("input", e=>{ |
| |
| 1989 | − clearTimeout(t); t=setTimeout(()=>{ state.q=e.target.value.trim(); load(); }, 280); |
| |
| 1990 | − }); |
| |
| 1991 | − $("region").onchange = e => { state.region = e.target.value; load(); }; |
| |
| 1992 | − $("period").onchange = e => { setPeriod(e.target.value); load(); }; |
| |
| 1993 | − $("more").onclick = () => { state.offset += state.limit; load(false); }; |
| |
| 1994 | − bindNav(document); |
| |
| 1995 | − |
| |
| 1996 | − await load(); |
| |
| 1997 | route(); |
2770 | route(); |
| 1998 | } |
2771 | } |
| 1999 | init(); |
2772 | init(); |