SPB Git

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

30.4 KB · 587 lines html
1<!--2  Author: Simon-Pierre Boucher <contact@spboucher.ai>3  File:   index.html4  Desc:   Frontend Sorti-Ka — recherche + fiches événement (SPA sans build,5          routeur History API, consomme /api). Design system Groupe KA6          « éditorial sharp » : papier #f5f3ee · encre #141814 · vert #1c5c41 ·7          lime #d9f26b · Space Grotesk / Inter / JetBrains Mono · bordures8          encre + ombres décalées + grain de film. SSR : sortika/seo.py9          injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY.10-->11<!DOCTYPE html>12<html lang="fr">13<head>14<meta charset="utf-8">15<meta name="viewport" content="width=device-width, initial-scale=1">16<title>Sorti·Ka — Toutes les sorties et tous les événements du Québec</title>17<meta name="description" content="Agrégateur indépendant de sorties et d'événements — concerts, festivals, spectacles, expositions, famille — dans les 17 régions du Québec, avec dates, lieux et gratuité.">18<!--SSR_HEAD-->19<link rel="preconnect" href="https://fonts.googleapis.com">20<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>21<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">22<style>23:root{24  /* système de design Groupe KA « éditorial sharp » */25  --paper:#f5f3ee; --surface:#fff; --surface-2:#faf9f5;26  --ink:#141814; --ink-2:#4d5551; --ink-3:#8b928c;27  --line:rgba(20,24,20,.14); --line-strong:rgba(20,24,20,.85);28  --green:#1c5c41; --green-deep:#123f2e;29  --lime:#d9f26b; --lime-soft:#f0f9d2;30  --amber:#e8a33d; --amber-soft:#fdf3e2;31  --danger:#b3423a; --danger-soft:#fbe9e7;32  --r-card:10px; --r-ctl:6px;33  --shadow-flat:0 1px 2px rgba(20,24,20,.05);34  --shadow-off:6px 6px 0 var(--ink);35  --shadow-off-soft:8px 8px 0 rgba(20,24,20,.08);36  --font-display:"Space Grotesk",system-ui,sans-serif;37  --font-body:"Inter",system-ui,sans-serif;38  --font-mono:"JetBrains Mono",ui-monospace,monospace;39}40*{box-sizing:border-box}41html{scroll-behavior:smooth}42body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font-body);43  font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased}44body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.35;z-index:9999;45  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")}46body>*{position:relative}47h1,h2,h3,h4{font-family:var(--font-display);letter-spacing:-.03em;margin:0}48a{color:inherit;text-decoration:none}49button{font-family:inherit}50img{display:block}51::selection{background:var(--lime);color:var(--ink)}52:focus-visible{outline:2px solid var(--green);outline-offset:2px}53.container{max-width:1240px;margin:0 auto;padding:0 24px}54@media(max-width:640px){.container{padding:0 16px}}5556/* ---------- micro-typo signature ---------- */57.kicker{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono);58  font-size:11.5px;font-weight:500;text-transform:uppercase;letter-spacing:.12em;color:var(--green)}59.kicker::before{content:"";width:22px;height:2px;background:var(--green)}60.klabel{font-family:var(--font-mono);font-size:10px;font-weight:700;61  text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3)}62.hl{display:inline-block;background:var(--lime);border-radius:8px;padding:0 10px 2px;transform:rotate(-1deg)}6364/* ---------- cartes & boutons signature ---------- */65.gk-card{background:var(--surface);border:1.5px solid var(--ink);66  border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)}67.gk-hover{transition:transform .18s ease,box-shadow .18s ease}68.gk-hover:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)}69.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;70  padding:10px 20px;border:1.5px solid var(--ink);border-radius:var(--r-ctl);71  font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer;72  transition:background .15s ease,transform .1s ease,box-shadow .1s ease}73.btn:active{transform:translate(2px,2px);box-shadow:none!important}74.btn-primary{background:var(--ink);color:var(--lime);box-shadow:4px 4px 0 rgba(20,24,20,.25)}75.btn-primary:hover{background:var(--green-deep)}76.btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)}77.btn-ghost:hover{background:var(--lime-soft)}7879/* ---------- header / ticker ---------- */80.ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden;81  font-family:var(--font-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;82  padding:7px 0;white-space:nowrap}83.ticker-track{display:inline-flex;animation:ticker 48s linear infinite;will-change:transform}84.ticker span{padding:0 26px;position:relative}85.ticker span::after{content:"◆";position:absolute;right:-6px;color:var(--lime);font-size:8px;top:3px}86@keyframes ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}}87@media(prefers-reduced-motion:reduce){.ticker-track{animation:none}*{transition-duration:.01ms!important}}88.header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9);89  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1.5px solid var(--ink)}90.header-inner{display:flex;align-items:center;gap:18px;height:64px}91.brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em;92  display:flex;align-items:baseline;line-height:1;cursor:pointer}93.brand .ka{background:var(--lime);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)}94.brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;95  letter-spacing:.08em;color:var(--ink-3);margin-left:8px}96@media(max-width:900px){.brand-tag{display:none}}97.nav{margin-left:auto;display:flex;gap:6px}98.nav a{padding:8px 14px;border:1.5px solid transparent;border-radius:var(--r-ctl);99  font-family:var(--font-display);font-weight:700;font-size:13.5px;transition:all .15s ease}100.nav a:hover{border-color:var(--ink);background:var(--lime-soft)}101102/* ---------- hero ---------- */103.hero{padding:56px 0 20px}104@media(max-width:640px){.hero{padding:34px 0 12px}}105.hero h1{font-size:clamp(34px,5.2vw,58px);font-weight:700;line-height:1.05;max-width:940px;margin-top:16px}106.hero p.lede{color:var(--ink-2);font-size:16.5px;max-width:660px;margin:18px 0 0}107.stat-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px}108.stat-chip{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-ctl);109  padding:8px 14px;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-2);110  display:flex;gap:8px;align-items:center;box-shadow:3px 3px 0 rgba(20,24,20,.12)}111.stat-chip b{color:var(--ink);font-weight:700}112.stat-chip .pulse{width:8px;height:8px;border-radius:50%;background:var(--green);113  box-shadow:0 0 0 4px var(--lime-soft);animation:pulse 2.4s ease infinite}114@keyframes pulse{50%{box-shadow:0 0 0 7px rgba(28,92,65,.16)}}115116/* ---------- filtres ---------- */117.filterbar{padding:14px;margin:28px 0 6px}118.f-primary{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap}119.f-search{flex:1 1 240px;min-width:0;max-width:460px;display:flex;align-items:center;gap:8px;120  border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface-2);121  padding:0 14px;min-height:46px;transition:background .15s ease,box-shadow .15s ease}122.f-search:focus-within{background:var(--surface);box-shadow:3px 3px 0 var(--lime)}123.f-search input{border:none;background:none;outline:none;flex:1;min-width:0;124  font-size:14px;color:var(--ink);font-family:inherit}125.f-ctl{display:flex;flex-direction:column;justify-content:center;gap:2px;126  border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface);127  padding:6px 14px 5px;min-height:46px;cursor:pointer;transition:box-shadow .15s ease}128.f-ctl:focus-within{box-shadow:3px 3px 0 var(--lime)}129.f-ctl select{border:none;background:none;outline:none;font-family:var(--font-display);130  font-size:13.5px;font-weight:700;color:var(--ink);padding:0;margin-left:-2px;max-width:220px}131.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}132.chip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl);133  padding:6px 12px;font-family:var(--font-display);font-size:12.5px;font-weight:700;134  cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease}135.chip:hover{background:var(--lime-soft)}136.chip.on{background:var(--ink);color:var(--lime);box-shadow:3px 3px 0 rgba(20,24,20,.25)}137.chip.free.on{background:var(--green);border-color:var(--green);color:#fff}138139/* ---------- résultats ---------- */140.meta-line{display:flex;align-items:center;gap:10px;margin:20px 2px 10px;141  font-family:var(--font-mono);font-size:11.5px;text-transform:uppercase;142  letter-spacing:.08em;color:var(--ink-3)}143.meta-line b{color:var(--ink)}144.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;margin:8px 0 34px}145.card{overflow:hidden;display:flex;flex-direction:column;cursor:pointer}146.card-img{position:relative;height:150px;border-bottom:1.5px solid var(--ink);147  background:repeating-linear-gradient(-45deg,var(--lime-soft),var(--lime-soft) 12px,var(--surface-2) 12px,var(--surface-2) 24px);overflow:hidden}148.card-img img{width:100%;height:100%;object-fit:cover}149.badge-date{position:absolute;left:10px;bottom:10px;font-family:var(--font-mono);150  font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;151  color:var(--ink);background:var(--lime);border:1.5px solid var(--ink);152  border-radius:var(--r-ctl);padding:4px 9px;white-space:nowrap;box-shadow:2px 2px 0 var(--ink)}153.card-body{padding:15px 16px 14px;display:flex;flex-direction:column;gap:8px;flex:1}154.card-top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}155.badge-free{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;156  color:#fff;background:var(--green);border-radius:var(--r-ctl);padding:4px 8px;flex:none}157.badge-price{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;158  color:var(--ink);background:var(--amber-soft);border:1px solid var(--amber);159  border-radius:var(--r-ctl);padding:3px 8px;flex:none}160.card h3{font-size:17px;font-weight:700;line-height:1.25}161.card .desc{font-size:12.5px;color:var(--ink-2);display:-webkit-box;162  -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}163.card .where{font-size:12.5px;color:var(--ink-3)}164.card .where b{color:var(--ink-2);font-weight:600}165.card .cats{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:4px}166.cat{font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.06em;167  text-transform:uppercase;color:var(--ink-2);border:1px solid var(--line-strong);168  border-radius:var(--r-ctl);padding:2px 7px}169.card .src{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;170  letter-spacing:.04em;color:var(--ink-3);border-top:1px dashed var(--line-strong);171  padding-top:10px;margin-top:6px;display:flex;justify-content:space-between}172.card .src .go{color:var(--green);font-weight:700}173.more{display:block;margin:0 auto 54px}174.empty{padding:60px 10px;text-align:center;color:var(--ink-3);grid-column:1/-1}175.empty b{font-family:var(--font-display);font-size:19px;color:var(--ink);display:block;margin-bottom:6px}176177/* ---------- fiche événement ---------- */178.detail{padding:34px 0 50px}179.back{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);180  font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;181  color:var(--green);cursor:pointer;margin-bottom:20px}182.back:hover{text-decoration:underline}183.detail-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:26px;align-items:start}184@media(max-width:860px){.detail-grid{grid-template-columns:1fr}}185.detail-hero{overflow:hidden}186.detail-hero .img{height:340px;border-bottom:1.5px solid var(--ink);187  background:repeating-linear-gradient(-45deg,var(--lime-soft),var(--lime-soft) 14px,var(--surface-2) 14px,var(--surface-2) 28px)}188.detail-hero .img img{width:100%;height:100%;object-fit:cover}189.detail-hero .body{padding:24px 26px 26px}190.detail-hero h1{font-size:clamp(26px,3.4vw,40px);line-height:1.08;margin:10px 0 14px}191.detail-hero .desc{color:var(--ink-2);font-size:15px;white-space:pre-line}192.detail-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}193.side{display:flex;flex-direction:column;gap:16px}194.side .box{padding:18px 20px}195.fact{display:flex;justify-content:space-between;gap:14px;padding:9px 0;border-bottom:1px dashed var(--line-strong);font-size:13.5px}196.fact:last-child{border-bottom:none}197.fact .k{font-family:var(--font-mono);font-size:10px;font-weight:700;198  text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding-top:3px;flex:none}199.fact .v{text-align:right;font-weight:600}200.side .btn{width:100%}201.maplink{font-family:var(--font-mono);font-size:11px;color:var(--green);font-weight:700;text-transform:uppercase}202.maplink:hover{text-decoration:underline}203.similar h2{font-size:22px;margin:44px 0 4px}204205/* ---------- sources / footer ---------- */206.sources{background:var(--surface-2);border-top:1.5px solid var(--ink);padding:44px 0 38px}207.sources h2{font-size:26px;margin-top:12px}208.sources p.lede2{color:var(--ink-2);font-size:14.5px;max-width:720px;margin:10px 0 0}209.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-top:22px}210.src-card{padding:16px 18px}211.src-card h4{font-size:15px;margin-bottom:5px}212.src-card .lic{font-family:var(--font-mono);font-size:9.5px;font-weight:700;213  color:var(--green);text-transform:uppercase;letter-spacing:.06em}214.src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px}215.src-card .n b{color:var(--green-deep)}216footer{background:var(--ink);color:rgba(245,243,238,.72);padding:36px 0 46px;font-size:13px}217footer .fam{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:14px}218footer .fam a{font-family:var(--font-display);font-weight:700;color:#f5f3ee;padding:4px 0}219footer .fam a:hover{color:var(--lime)}220footer .fam .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;221  letter-spacing:.08em;color:rgba(245,243,238,.45);align-self:center}222footer a.mail{color:var(--lime)}223#ssr{display:none}224.hidden{display:none!important}225</style>226</head>227<body>228229<div id="ssr"><!--SSR_BODY--></div>230231<div class="ticker"><div class="ticker-track">232  <span>Concerts</span><span>Festivals</span><span>Humour</span><span>Théâtre</span>233  <span>Expositions</span><span>Danse</span><span>Sport</span><span>Famille</span>234  <span>Plein air</span><span>17 régions du Québec</span>235  <span>Concerts</span><span>Festivals</span><span>Humour</span><span>Théâtre</span>236  <span>Expositions</span><span>Danse</span><span>Sport</span><span>Famille</span>237  <span>Plein air</span><span>17 régions du Québec</span>238</div></div>239240<header class="header">241  <div class="container header-inner">242    <a class="brand" data-nav href="/">Sorti<span class="ka">·Ka</span></a>243    <span class="brand-tag">Sorties &amp; événements — Québec</span>244    <nav class="nav">245      <a href="/#sources">Sources</a>246      <a href="/docs" target="_blank" rel="noopener">API</a>247    </nav>248  </div>249</header>250251<!-- ======== VUE LISTE ======== -->252<main id="view-list">253  <section class="hero container">254    <span class="kicker">Agrégateur indépendant · mise à jour horaire</span>255    <h1>Toutes les sorties du Québec, <span class="hl">un seul endroit.</span></h1>256    <p class="lede">Concerts, festivals, spectacles, expositions, sport, famille — agrégés257       depuis des billetteries et des sources ouvertes officielles, dans les 17 régions.258       Chaque fiche renvoie vers la billetterie ou la source originale.</p>259    <div class="stat-row" id="stats"></div>260  </section>261262  <section class="container">263    <div class="filterbar gk-card">264      <div class="f-primary">265        <label class="f-search">🔎<input type="text" id="q" placeholder="Artiste, événement, salle, ville…"></label>266        <label class="f-ctl"><span class="klabel">Région</span>267          <select id="region"><option value="">Toutes les régions</option></select>268        </label>269        <label class="f-ctl"><span class="klabel">Quand</span>270          <select id="period">271            <option value="">À venir (tout)</option>272            <option value="today">Aujourd'hui</option>273            <option value="weekend">Ce week-end</option>274            <option value="7">7 prochains jours</option>275            <option value="30">30 prochains jours</option>276          </select>277        </label>278      </div>279      <div class="chips" id="cats"></div>280    </div>281282    <div class="meta-line"><span id="count">Chargement…</span></div>283    <section class="grid" id="grid"></section>284    <button class="more btn btn-primary" id="more" hidden>Afficher plus d'événements</button>285  </section>286287  <section class="sources" id="sources">288    <div class="container">289      <span class="kicker">Index fidèle · attribution systématique</span>290      <h2>Les sources</h2>291      <p class="lede2">Rien d'inventé : chaque événement est attribué à sa source et lié292         vers la fiche originale. Billetteries au robots.txt permissif et293         <b>données ouvertes officielles</b> (licences Creative Commons).</p>294      <div class="src-grid" id="src-grid"></div>295    </div>296  </section>297</main>298299<!-- ======== VUE FICHE ÉVÉNEMENT ======== -->300<main id="view-detail" class="hidden">301  <div class="container detail">302    <span class="back" id="back">← Toutes les sorties</span>303    <div class="detail-grid">304      <article class="gk-card detail-hero">305        <div class="img" id="d-img"></div>306        <div class="body">307          <span class="klabel" id="d-cats-label"></span>308          <h1 id="d-title"></h1>309          <div class="desc" id="d-desc"></div>310          <div class="detail-cats" id="d-cats"></div>311        </div>312      </article>313      <aside class="side">314        <div class="gk-card box" id="d-facts"></div>315        <a class="btn btn-primary" id="d-cta" target="_blank" rel="noopener">Billets · fiche originale ↗</a>316        <div class="gk-card box" id="d-src"></div>317      </aside>318    </div>319    <section class="similar">320      <span class="kicker">À voir aussi</span>321      <h2 id="d-similar-title">Événements similaires</h2>322      <div class="grid" id="d-similar"></div>323    </section>324  </div>325</main>326327<footer>328  <div class="container">329    <div class="fam">330      <span class="lbl">Famille ·Ka</span>331      <a href="https://www.lou-ka.com" target="_blank" rel="noopener">Lou·Ka</a>332      <a href="https://www.immo-ka.com" target="_blank" rel="noopener">Immo·Ka</a>333      <a href="https://www.auto-ka.com" target="_blank" rel="noopener">Auto·Ka</a>334      <a href="https://www.food-ka.com" target="_blank" rel="noopener">Food·Ka</a>335      <a href="https://www.fabri-ka.com" target="_blank" rel="noopener">Fabri·Ka</a>336      <a href="https://www.toit-ka.com" target="_blank" rel="noopener">Toit·Ka</a>337      <a href="https://www.forma-ka.com" target="_blank" rel="noopener">Forma·Ka</a>338      <a href="https://www.groupe-ka.com" target="_blank" rel="noopener">Groupe KA</a>339    </div>340    <div>Données : SIT Québec (MTO, CC 4.0) · Ville de Montréal (CC BY 4.0) · Ville de Laval (CC 4.0) ·341         Le point de vente · evenko · Atuvu.ca · La Vitrine · Répertoire des municipalités (MAMH).</div>342    <div style="margin-top:8px">© 2026 Sorti·Ka — Simon-Pierre Boucher ·343      <a class="mail" href="mailto:contact@spboucher.ai">contact@spboucher.ai</a></div>344  </div>345</footer>346347<script>348/* Sorti-Ka — frontend + routeur. Auteur : Simon-Pierre Boucher <contact@spboucher.ai> */349"use strict";350const $ = id => document.getElementById(id);351const state = { q:"", region:"", category:"", free:false, from:"", to:"", offset:0, limit:30, total:0 };352353const CAT_LABELS = {354  "festival":"Festivals","musique":"Musique","arts-scene":"Arts de la scène",355  "exposition-musee":"Expos & musées","cinema":"Cinéma","sport":"Sport",356  "plein-air":"Plein air","famille":"Famille","gastronomie":"Gastronomie",357  "marche-foire":"Marchés & foires","conference":"Conférences & ateliers",358  "communautaire":"Communautaire","patrimoine":"Patrimoine","autre":"Autre"359};360const SRC_LABELS = {361  "sitq":"SIT Québec — Ministère du Tourisme","montreal":"Ville de Montréal",362  "laval":"Ville de Laval","lepointdevente":"Le point de vente",363  "evenko":"evenko","atuvu":"Atuvu.ca","lavitrine":"La Vitrine","sherbrooke":"Ville de Sherbrooke"364};365const MONTHS = ["jan","fév","mar","avr","mai","juin","juil","août","sep","oct","nov","déc"];366const MONTHS_FULL = ["janvier","février","mars","avril","mai","juin","juillet",367  "août","septembre","octobre","novembre","décembre"];368369function fmtDate(iso, full=false){370  if(!iso) return "Date à confirmer";371  const [y,m,d] = iso.split("-").map(Number);372  return `${d} ${(full?MONTHS_FULL:MONTHS)[m-1]} ${y}`;373}374function dateBadge(ev, full=false){375  if(!ev.start_date) return "Date à confirmer";376  if(ev.end_date && ev.end_date !== ev.start_date)377    return `${fmtDate(ev.start_date, full)} → ${fmtDate(ev.end_date, full)}`;378  return fmtDate(ev.start_date, full);379}380function esc(s){ const d=document.createElement("div"); d.textContent=s||""; return d.innerHTML; }381function priceBadge(ev){382  if(ev.is_free === true) return `<span class="badge-free">Gratuit</span>`;383  if(ev.price_min != null) return `<span class="badge-price">Dès ${ev.price_min.toFixed(2).replace(".",",")} $</span>`;384  if(ev.is_free === false) return `<span class="badge-price">Payant</span>`;385  return "";386}387388function card(ev){389  const where = [ev.venue, ev.city, ev.region].filter(Boolean)390    .map((x,i)=> i===1?`<b>${esc(x)}</b>`:esc(x)).join(" · ");391  const cats = (ev.categories||[]).filter(c=>c!=="autre").slice(0,3)392    .map(c=>`<span class="cat">${esc(CAT_LABELS[c]||c)}</span>`).join("");393  const img = ev.image394    ? `<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">` : "";395  return `<a class="card gk-card gk-hover" data-nav href="/evenement/${esc(ev.uid)}">396    <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span></div>397    <div class="card-body">398      <div class="card-top"><h3>${esc(ev.title)}</h3>${priceBadge(ev)}</div>399      ${ev.description?`<div class="desc">${esc(ev.description)}</div>`:""}400      <div class="where">${where||"Lieu à confirmer"}</div>401      <div class="cats">${cats}</div>402      <div class="src"><span>${SRC_LABELS[ev.source]||ev.source}</span><span class="go">Fiche →</span></div>403    </div>404  </a>`;405}406407function params(){408  const p = new URLSearchParams();409  if(state.q) p.set("q", state.q);410  if(state.region) p.set("region", state.region);411  if(state.category) p.set("category", state.category);412  if(state.free) p.set("free","true");413  if(state.from) p.set("from", state.from);414  if(state.to) p.set("to", state.to);415  p.set("limit", state.limit); p.set("offset", state.offset);416  return p.toString();417}418419async function load(reset=true){420  if(reset){ state.offset = 0; }421  const data = await (await fetch(`/api/events?${params()}`)).json();422  state.total = data.total;423  const html = data.events.map(card).join("");424  if(reset){425    $("grid").innerHTML = html ||426      `<div class="empty"><b>Aucun événement trouvé</b>Essayez d'élargir les filtres ou la période.</div>`;427  } else {428    $("grid").insertAdjacentHTML("beforeend", html);429  }430  $("count").innerHTML = `<b>${data.total.toLocaleString("fr-CA")}</b>&nbsp;événement${data.total>1?"s":""} à venir`;431  $("more").hidden = state.offset + data.events.length >= data.total;432  bindNav($("grid"));433}434435function setPeriod(v){436  const today = new Date(); const iso = d => d.toISOString().slice(0,10);437  state.from = ""; state.to = "";438  if(v==="today"){ state.from = iso(today); state.to = iso(today); }439  else if(v==="weekend"){440    const day = today.getDay();441    const sat = new Date(today);442    if(day !== 6 && day !== 0) sat.setDate(today.getDate() + (6 - day));443    const sun = new Date(sat); sun.setDate(sat.getDate() + (day === 0 ? 0 : 1));444    state.from = iso(day===6||day===0 ? today : sat); state.to = iso(sun);445  }446  else if(v){ const end = new Date(today); end.setDate(today.getDate()+Number(v)); state.from = iso(today); state.to = iso(end); }447}448449/* ---------- fiche événement ---------- */450async function showDetail(uid){451  let ev;452  try{ ev = await (await fetch(`/api/events/${encodeURIComponent(uid)}`)).json(); }453  catch(e){ go("/"); return; }454  if(ev.detail){ go("/"); return; }455456  document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`;457  $("d-img").innerHTML = ev.image458    ? `<img src="${esc(ev.image)}" alt="" onerror="this.remove()">` : "";459  $("d-cats-label").textContent = (ev.categories||[]).filter(c=>c!=="autre")460    .map(c=>CAT_LABELS[c]||c).join(" · ") || "Événement";461  $("d-title").textContent = ev.title;462  $("d-desc").textContent = ev.description || "";463  $("d-cats").innerHTML = (ev.raw_categories||[]).slice(0,5).filter(c=>c.length<40)464    .map(c=>`<span class="cat">${esc(c)}</span>`).join("");465466  const facts = [];467  facts.push(["Quand", dateBadge(ev, true)]);468  if(ev.venue) facts.push(["Lieu", esc(ev.venue)]);469  if(ev.address) facts.push(["Adresse", esc(ev.address)]);470  if(ev.city) facts.push(["Ville", esc(ev.city)]);471  if(ev.region) facts.push(["Région", esc(ev.region)]);472  if(ev.is_free === true) facts.push(["Prix", `<span class="badge-free">Gratuit</span>`]);473  else if(ev.price_min != null) facts.push(["Prix", `Dès ${ev.price_min.toFixed(2).replace(".",",")} $ CAD`]);474  else if(ev.is_free === false) facts.push(["Prix", "Payant — voir la billetterie"]);475  if(ev.price_label) facts.push(["Détail prix", esc(ev.price_label)]);476  if(ev.audience) facts.push(["Public", esc(ev.audience)]);477  if(ev.organizer) facts.push(["Organisé par", esc(ev.organizer)]);478  if(ev.lat && ev.lng) facts.push(["Carte",479    `<a class="maplink" target="_blank" rel="noopener" href="https://www.google.com/maps?q=${ev.lat},${ev.lng}">Voir sur la carte ↗</a>`]);480  $("d-facts").innerHTML = facts.map(([k,v]) =>481    `<div class="fact"><span class="k">${k}</span><span class="v">${v}</span></div>`).join("");482483  $("d-cta").href = ev.url;484  $("d-src").innerHTML = `<span class="klabel">Source & attribution</span>485    <div style="margin-top:6px;font-size:13px">${SRC_LABELS[ev.source]||ev.source}</div>486    <div style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3);margin-top:4px">487      Capté le ${new Date((ev.updated_at||0)*1000).toLocaleDateString("fr-CA")} ·488      identifiant ${esc(ev.uid)}</div>`;489490  // événements similaires : même catégorie prioritaire, sinon même ville491  const cat = (ev.categories||[]).filter(c=>c!=="autre")[0] || "";492  const q = new URLSearchParams({limit: 7});493  if(cat) q.set("category", cat); else if(ev.city) q.set("city", ev.city);494  if(ev.region && cat) q.set("region", ev.region);495  try{496    const sim = await (await fetch(`/api/events?${q}`)).json();497    const others = sim.events.filter(e=>e.uid!==ev.uid).slice(0,6);498    $("d-similar").innerHTML = others.map(card).join("") ||499      `<div class="empty">Rien de similaire pour l'instant.</div>`;500    bindNav($("d-similar"));501  }catch(e){ $("d-similar").innerHTML = ""; }502503  $("view-list").classList.add("hidden");504  $("view-detail").classList.remove("hidden");505  window.scrollTo({top:0});506}507508function showList(){509  document.title = "Sorti·Ka — Toutes les sorties et tous les événements du Québec";510  $("view-detail").classList.add("hidden");511  $("view-list").classList.remove("hidden");512}513514/* ---------- routeur ---------- */515function route(){516  const m = location.pathname.match(/^\/evenement\/(.+)$/);517  if(m) showDetail(decodeURIComponent(m[1]));518  else showList();519}520function go(path){521  history.pushState({}, "", path);522  route();523}524function bindNav(root){525  (root||document).querySelectorAll("a[data-nav]").forEach(a => a.onclick = e => {526    if(e.metaKey||e.ctrlKey||e.shiftKey) return;527    e.preventDefault(); go(a.getAttribute("href"));528  });529}530window.addEventListener("popstate", route);531$("back").onclick = () => go("/");532533/* ---------- init ---------- */534async function init(){535  const regions = await (await fetch("/api/regions")).json();536  $("region").insertAdjacentHTML("beforeend",537    regions.map(r=>`<option>${esc(r)}</option>`).join(""));538539  const cats = await (await fetch("/api/categories")).json();540  $("cats").innerHTML =541    `<span class="chip free" id="chip-free">✦ Gratuit</span>` +542    cats.filter(c=>c!=="autre").map(c=>`<span class="chip" data-cat="${c}">${CAT_LABELS[c]||c}</span>`).join("");543  $("chip-free").onclick = e => { state.free=!state.free; e.target.classList.toggle("on",state.free); load(); };544  document.querySelectorAll(".chip[data-cat]").forEach(ch => ch.onclick = () => {545    const c = ch.dataset.cat;546    state.category = state.category===c ? "" : c;547    document.querySelectorAll(".chip[data-cat]").forEach(x=>x.classList.toggle("on", x.dataset.cat===state.category));548    load();549  });550551  try{552    const s = await (await fetch("/api/stats")).json();553    const nReg = Object.keys(s.by_region).filter(r=>r!=="Non rattachée").length;554    $("stats").innerHTML = [555      `<span class="stat-chip"><span class="pulse"></span><b>${s.upcoming.toLocaleString("fr-CA")}</b> événements à venir</span>`,556      `<span class="stat-chip"><b>${s.free_upcoming.toLocaleString("fr-CA")}</b> gratuits</span>`,557      `<span class="stat-chip"><b>${nReg}</b> régions</span>`,558      `<span class="stat-chip"><b>${s.cities.toLocaleString("fr-CA")}</b> villes</span>`,559    ].join("");560  }catch(e){}561562  try{563    const reg = await (await fetch("/api/sources")).json();564    $("src-grid").innerHTML = (reg.sources||[]).filter(s=>s.statut==="actif"||s.active_events>0).map(s=>`565      <div class="src-card gk-card">566        <h4>${esc(s.name)}</h4>567        <div class="lic">${esc((s.acces_legal||"").split("—")[0])}</div>568        <div class="n"><b>${(s.active_events||0).toLocaleString("fr-CA")}</b> événements actifs · ${esc((s.extraction||"").split("(")[0])}</div>569      </div>`).join("");570  }catch(e){}571572  let t; $("q").addEventListener("input", e=>{573    clearTimeout(t); t=setTimeout(()=>{ state.q=e.target.value.trim(); load(); }, 280);574  });575  $("region").onchange = e => { state.region = e.target.value; load(); };576  $("period").onchange = e => { setPeriod(e.target.value); load(); };577  $("more").onclick = () => { state.offset += state.limit; load(false); };578  bindNav(document);579580  await load();581  route();582}583init();584</script>585</body>586</html>587
Files · 916445602476d223e0a907af13c548393ef1cebd
Menu

Theme