| 397 |
397 |
.st-empty{padding:20px;background:var(--surface-2);border-style:dashed} |
| 398 |
398 |
.st-empty b{font-family:var(--font-display);font-size:14px} |
| 399 |
399 |
.st-loading{padding:40px 0;font-family:var(--font-mono);font-size:12px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.08em} |
|
400 |
+/* ---- favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ---- */ |
|
401 |
+.fav-btn{position:absolute;top:8px;right:8px;z-index:2;width:34px;height:34px; |
|
402 |
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer; |
|
403 |
+ background:var(--surface);border:1.5px solid var(--ink);border-radius:50%; |
|
404 |
+ color:var(--ink);padding:0;transition:transform .12s ease,background .12s ease} |
|
405 |
+.fav-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round} |
|
406 |
+.fav-btn:hover{transform:scale(1.08);background:var(--accent-soft)} |
|
407 |
+.fav-btn.on{background:var(--accent);border-color:var(--accent-deep);color:var(--on-accent)} |
|
408 |
+.fav-btn.on svg{fill:currentColor} |
|
409 |
+.fav-btn.big{position:static;flex:none;width:44px;height:44px} |
|
410 |
+.fav-btn.big svg{width:21px;height:21px} |
|
411 |
+.d-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px} |
|
412 |
+.d-title-row h1{min-width:0} |
|
413 |
+.fav-empty{grid-column:1/-1} |
|
414 |
+.fav-empty .btn-ka{margin-top:12px} |
| 400 |
415 |
#ssr{display:none} |
| 401 |
416 |
.hidden{display:none!important} |
| 402 |
417 |
</style> |
| 423 |
438 |
<span class="brand-tag">Sorties & événements — Québec</span> |
| 424 |
439 |
<nav class="nav"> |
| 425 |
440 |
<a href="/#sources">Sources</a> |
|
441 |
+ <a data-nav href="/favoris" title="Mes favoris — Mon univers Ka">♥ Favoris</a> |
| 426 |
442 |
<a data-nav href="/stats">Stats</a> |
| 427 |
443 |
<a data-nav href="/contact">Contact</a> |
| 428 |
444 |
<a href="/docs" target="_blank" rel="noopener">API</a> |
| 489 |
505 |
<div class="body"> |
| 490 |
506 |
<span class="klabel" id="d-cats-label"></span> |
| 491 |
507 |
<div id="d-status"></div> |
| 492 |
|
− <h1 id="d-title"></h1> |
|
508 |
+ <div class="d-title-row"><h1 id="d-title"></h1><span id="d-fav"></span></div> |
| 493 |
509 |
<div class="desc" id="d-desc"></div> |
| 494 |
510 |
<div class="detail-cats" id="d-cats"></div> |
| 495 |
511 |
</div> |
| 508 |
524 |
</div> |
| 509 |
525 |
</main> |
| 510 |
526 |
|
|
527 |
+<!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== --> |
|
528 |
+<main id="view-favoris" class="hidden"> |
|
529 |
+ <section class="hero container"> |
|
530 |
+ <span class="kicker">Mon univers Ka · synchronisés sur toutes les plateformes Groupe KA</span> |
|
531 |
+ <h1>Mes <span class="hl">favoris</span></h1> |
|
532 |
+ <p class="lede" id="fav-lede">Vos sorties et événements favoris, rattachés à votre compte KA ID.</p> |
|
533 |
+ </section> |
|
534 |
+ <section class="container"> |
|
535 |
+ <div class="meta-line"><span id="fav-count"></span></div> |
|
536 |
+ <section class="grid" id="fav-grid"></section> |
|
537 |
+ </section> |
|
538 |
+</main> |
|
539 |
+ |
| 511 |
540 |
<!-- ======== VUE STATS (/stats) — module ka-stats (SPEC.md), vanilla + SVG ======== --> |
| 512 |
541 |
<main id="view-stats" class="hidden"> |
| 513 |
542 |
<div class="container stats-page"> |
| 724 |
753 |
const img = ev.image |
| 725 |
754 |
? `<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">` : ""; |
| 726 |
755 |
return `<a class="card gk-card gk-hover" data-nav href="/evenement/${esc(ev.uid)}"> |
| 727 |
|
− <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span></div> |
|
756 |
+ <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span>${favBtn(ev)}</div> |
| 728 |
757 |
<div class="card-body"> |
| 729 |
758 |
<div class="card-top"><h3>${esc(ev.title)}</h3>${statusBadge(ev)||priceBadge(ev)}</div> |
| 730 |
759 |
${ev.description?`<div class="desc">${esc(ev.description)}</div>`:""} |
| 795 |
824 |
: ev.status==="postponed" ? "Cet événement est reporté — vérifiez la nouvelle date chez la source" |
| 796 |
825 |
: "Complet — plus de billets disponibles"}</div>` : ""; |
| 797 |
826 |
$("d-title").textContent = ev.title; |
|
827 |
+ $("d-fav").innerHTML = favBtn(ev, true); |
| 798 |
828 |
$("d-desc").textContent = ev.description || ""; |
| 799 |
829 |
$("d-cats").innerHTML = (ev.raw_categories||[]).slice(0,5).filter(c=>c.length<40) |
| 800 |
830 |
.map(c=>`<span class="cat">${esc(c)}</span>`).join(""); |
| 1598 |
1628 |
|
| 1599 |
1629 |
$("st-custom-btn").onclick = () => { SB.cat = null; SB.err = ""; sbOpen(); }; |
| 1600 |
1630 |
|
|
1631 |
+/* ---------- favoris ♥ « Mon univers Ka » ---------- |
|
1632 |
+ Le hub Groupe KA (groupe-ka.com) est le magasin central des favoris du |
|
1633 |
+ groupe : AUCUN stockage navigateur. Lecture/écriture via l'API locale |
|
1634 |
+ /api/favorites, qui relaie au hub (session KA ID requise). */ |
|
1635 |
+const FAV = {ready:false, connected:false, ids:new Set(), items:[], meta:{}}; |
|
1636 |
+const FAV_HEART = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`; |
|
1637 |
+ |
|
1638 |
+/* événement -> item de favori poussé au hub (titre, lieu · date, prix, image) */ |
|
1639 |
+function favItem(ev){ |
|
1640 |
+ let price = ""; |
|
1641 |
+ if(ev.is_free === true) price = "Gratuit"; |
|
1642 |
+ else if(ev.price_min != null) price = `Dès ${ev.price_min.toFixed(2).replace(".",",")} $`; |
|
1643 |
+ return { |
|
1644 |
+ item_id: ev.uid, |
|
1645 |
+ title: ev.title || "Événement", |
|
1646 |
+ subtitle: [ev.venue || ev.city, dateBadge(ev)].filter(Boolean).join(" · "), |
|
1647 |
+ price_label: price, |
|
1648 |
+ image_url: ev.image || "", |
|
1649 |
+ url: `https://www.sorti-ka.com/evenement/${encodeURIComponent(ev.uid)}`, |
|
1650 |
+ }; |
|
1651 |
+} |
|
1652 |
+ |
|
1653 |
+/* cœur ♥ — cartes (par défaut) et fiche événement (big) */ |
|
1654 |
+function favBtn(ev, big=false){ |
|
1655 |
+ FAV.meta[ev.uid] = favItem(ev); |
|
1656 |
+ const on = FAV.ids.has(ev.uid); |
|
1657 |
+ const label = on ? "Retirer de mes favoris (Mon univers Ka)" |
|
1658 |
+ : "Ajouter à mes favoris (Mon univers Ka)"; |
|
1659 |
+ return `<button type="button" class="fav-btn${on?" on":""}${big?" big":""}" |
|
1660 |
+ data-fav="${esc(ev.uid)}" aria-pressed="${on}" aria-label="${label}" |
|
1661 |
+ title="${label}">${FAV_HEART}</button>`; |
|
1662 |
+} |
|
1663 |
+ |
|
1664 |
+/* repeint tous les cœurs affichés selon l'état courant */ |
|
1665 |
+function favPaint(){ |
|
1666 |
+ document.querySelectorAll(".fav-btn[data-fav]").forEach(b => { |
|
1667 |
+ const on = FAV.ids.has(b.dataset.fav); |
|
1668 |
+ b.classList.toggle("on", on); |
|
1669 |
+ b.setAttribute("aria-pressed", on); |
|
1670 |
+ const label = on ? "Retirer de mes favoris (Mon univers Ka)" |
|
1671 |
+ : "Ajouter à mes favoris (Mon univers Ka)"; |
|
1672 |
+ b.setAttribute("aria-label", label); b.title = label; |
|
1673 |
+ }); |
|
1674 |
+} |
|
1675 |
+ |
|
1676 |
+/* lit la liste au hub (via l'API locale) — 401 = pas connecté, cœurs vides */ |
|
1677 |
+async function favLoad(){ |
|
1678 |
+ try{ |
|
1679 |
+ const r = await fetch("/api/favorites"); |
|
1680 |
+ if(r.status === 401){ FAV.connected = false; return; } |
|
1681 |
+ if(!r.ok) return; |
|
1682 |
+ const d = await r.json(); |
|
1683 |
+ FAV.connected = true; |
|
1684 |
+ FAV.items = d.items || []; |
|
1685 |
+ FAV.ids = new Set(d.ids || []); |
|
1686 |
+ favPaint(); |
|
1687 |
+ }catch(e){} |
|
1688 |
+ finally{ FAV.ready = true; } |
|
1689 |
+} |
|
1690 |
+ |
|
1691 |
+/* toggle optimiste — le hub confirme (sinon on rétablit) */ |
|
1692 |
+async function favToggle(uid){ |
|
1693 |
+ if(!FAV.connected){ location.href = "/api/auth/ka/login"; return; } |
|
1694 |
+ const on = !FAV.ids.has(uid); |
|
1695 |
+ const item = FAV.meta[uid] || FAV.items.find(i => i.item_id === uid) || {item_id: uid}; |
|
1696 |
+ const apply = v => { |
|
1697 |
+ if(v){ FAV.ids.add(uid); if(!FAV.items.some(i=>i.item_id===uid)) FAV.items.push(item); } |
|
1698 |
+ else { FAV.ids.delete(uid); FAV.items = FAV.items.filter(i => i.item_id !== uid); } |
|
1699 |
+ favPaint(); |
|
1700 |
+ if(location.pathname === "/favoris") renderFavs(); |
|
1701 |
+ }; |
|
1702 |
+ apply(on); |
|
1703 |
+ try{ |
|
1704 |
+ const r = await fetch("/api/favorites/toggle", {method:"POST", |
|
1705 |
+ headers:{"Content-Type":"application/json"}, |
|
1706 |
+ body: JSON.stringify({on, item})}); |
|
1707 |
+ if(!r.ok) throw new Error(r.status); |
|
1708 |
+ }catch(e){ apply(!on); } |
|
1709 |
+} |
|
1710 |
+ |
|
1711 |
+/* clic sur un cœur — en phase capture : la carte entière est un lien */ |
|
1712 |
+document.addEventListener("click", e => { |
|
1713 |
+ const b = e.target.closest(".fav-btn[data-fav]"); |
|
1714 |
+ if(!b) return; |
|
1715 |
+ e.preventDefault(); e.stopPropagation(); |
|
1716 |
+ favToggle(b.dataset.fav); |
|
1717 |
+}, true); |
|
1718 |
+ |
|
1719 |
+/* carte d'un favori (données du hub — l'événement peut avoir quitté l'index) */ |
|
1720 |
+function favCard(it){ |
|
1721 |
+ FAV.meta[it.item_id] = it; |
|
1722 |
+ const img = it.image_url |
|
1723 |
+ ? `<img src="${esc(it.image_url)}" alt="" loading="lazy" onerror="this.remove()">` : ""; |
|
1724 |
+ const local = (it.url || "").replace(/^https?:\/\/[^/]*sorti-ka\.com/i, "") |
|
1725 |
+ || `/evenement/${encodeURIComponent(it.item_id)}`; |
|
1726 |
+ return `<a class="card gk-card gk-hover" data-nav href="${esc(local)}"> |
|
1727 |
+ <div class="card-img">${img} |
|
1728 |
+ <button type="button" class="fav-btn on" data-fav="${esc(it.item_id)}" aria-pressed="true" |
|
1729 |
+ aria-label="Retirer de mes favoris (Mon univers Ka)" |
|
1730 |
+ title="Retirer de mes favoris (Mon univers Ka)">${FAV_HEART}</button> |
|
1731 |
+ </div> |
|
1732 |
+ <div class="card-body"> |
|
1733 |
+ <div class="card-top"><h3>${esc(it.title)}</h3>${it.price_label?`<span class="badge-price">${esc(it.price_label)}</span>`:""}</div> |
|
1734 |
+ ${it.subtitle?`<div class="where">${esc(it.subtitle)}</div>`:""} |
|
1735 |
+ <div class="src"><span>Mon univers Ka</span><span class="go">Fiche →</span></div> |
|
1736 |
+ </div> |
|
1737 |
+ </a>`; |
|
1738 |
+} |
|
1739 |
+ |
|
1740 |
+function renderFavs(){ |
|
1741 |
+ if(!FAV.connected){ |
|
1742 |
+ $("fav-count").innerHTML = "Connectez-vous pour retrouver vos favoris"; |
|
1743 |
+ $("fav-grid").innerHTML = `<div class="empty fav-empty"> |
|
1744 |
+ <b>Vos favoris vous suivent partout dans l'univers Ka</b> |
|
1745 |
+ Connectez-vous avec votre KA ID pour ajouter des sorties à vos favoris — |
|
1746 |
+ ils sont synchronisés sur toutes les plateformes du Groupe KA.<br> |
|
1747 |
+ <a class="btn-ka" href="/api/auth/ka/login"><span class="kdot">·Ka</span> Se connecter avec KA ID</a></div>`; |
|
1748 |
+ return; |
|
1749 |
+ } |
|
1750 |
+ const items = FAV.items; |
|
1751 |
+ $("fav-count").innerHTML = items.length |
|
1752 |
+ ? `<b>${items.length.toLocaleString("fr-CA")}</b> favori${items.length>1?"s":""} sur Sorti·Ka` |
|
1753 |
+ : "Aucun favori pour l'instant"; |
|
1754 |
+ $("fav-grid").innerHTML = items.map(favCard).join("") || |
|
1755 |
+ `<div class="empty fav-empty"><b>Aucun favori pour l'instant</b> |
|
1756 |
+ Touchez le cœur ♥ d'un événement pour le garder ici — et le retrouver |
|
1757 |
+ sur toutes les plateformes du Groupe KA.</div>`; |
|
1758 |
+ bindNav($("fav-grid")); |
|
1759 |
+} |
|
1760 |
+ |
|
1761 |
+async function showFavoris(){ |
|
1762 |
+ document.title = "Mes favoris — Sorti-Ka | Mon univers Ka"; |
|
1763 |
+ show("view-favoris"); |
|
1764 |
+ window.scrollTo({top:0}); |
|
1765 |
+ await favLoad(); // le hub est la source de vérité — relire à l'ouverture |
|
1766 |
+ renderFavs(); |
|
1767 |
+} |
|
1768 |
+ |
| 1601 |
1769 |
/* ---------- vues ---------- */ |
| 1602 |
|
−const VIEWS = ["view-list","view-detail","view-contact","view-stats"]; |
|
1770 |
+const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"]; |
| 1603 |
1771 |
function show(id){ |
| 1604 |
1772 |
VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id)); |
| 1605 |
1773 |
} |
| 1628 |
1796 |
const c = location.pathname.match(/^\/categorie\/([^/]+)$/); |
| 1629 |
1797 |
if(m) showDetail(decodeURIComponent(m[1])); |
| 1630 |
1798 |
else if(location.pathname === "/contact") showContact(); |
|
1799 |
+ else if(location.pathname === "/favoris") showFavoris(); |
| 1631 |
1800 |
else if(location.pathname === "/stats") showStats(); |
| 1632 |
1801 |
else if(r){ |
| 1633 |
1802 |
// page SSR /region/{slug} : la SPA ouvre la recherche filtrée sur la région |
| 1659 |
1828 |
|
| 1660 |
1829 |
/* ---------- init ---------- */ |
| 1661 |
1830 |
async function init(){ |
|
1831 |
+ favLoad(); // favoris ♥ — repeint les cœurs dès la réponse du hub |
| 1662 |
1832 |
const regions = await (await fetch("/api/regions")).json(); |
| 1663 |
1833 |
allRegions = regions; |
| 1664 |
1834 |
$("region").insertAdjacentHTML("beforeend", |