SPB Git

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

Favoris unifiés « Mon univers Ka » (KA ID) — hub groupe-ka.com

- creaka/hubfav.py : client HMAC du hub (GET liste cache 30 s, POST toggle
  synchrone, aucun stockage local) — magasin central des favoris du groupe
- creaka/web.py : routes GET /api/favorites + POST /api/favorites/toggle
  (session KA ID requise, 401 sinon), déclarées avant le fallback SPA
- frontend : cœur ♥ sur les cartes de créateurs et CTA sur la fiche
  (« Se connecter avec KA ID » si déconnecté), page /favoris (grille des
  favoris lus au hub, retrait en place), nav header + footer + tab bar
  mobile (5 onglets)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 23, 2026) parent 07480c1 Browse files

4 changed files +481 −12

added creaka/hubfav.py +103 −0
@@ -0,0 +1,103 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: creaka/hubfav.py
4+# Desc: Favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com) est le
5+# MAGASIN CENTRAL des favoris du groupe : Créa-Ka ne stocke rien
6+# localement. Chaque ♥ est poussé au hub (POST signé HMAC, synchrone :
7+# un échec remonte à l'appelant) et la liste est lue au hub (GET signé,
8+# cache mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.
9+# Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).
10+# ==============================================================================
11+from __future__ import annotations
12+
13+import hashlib
14+import hmac
15+import os
16+import threading
17+import time
18+
19+import requests
20+
21+CLIENT_ID = "crea-ka"
22+KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")
23+CACHE_TTL = 30 # secondes — liste des favoris
24+TIMEOUT = 6 # secondes
25+
26+_cache: dict[str, tuple[float, list[dict]]] = {}
27+_lock = threading.Lock()
28+
29+# champs d'item acceptés -> longueur maximale (troncature défensive)
30+_FIELDS = {"item_id": 120, "title": 200, "subtitle": 200,
31+ "price_label": 60, "image_url": 500, "url": 500}
32+
33+
34+def _sig(ka_id: str, ts: int) -> str | None:
35+ """Signature HMAC-SHA256 du hub : hex("crea-ka.<ka_id>.<ts>")."""
36+ secret = os.environ.get("KA_SSO_SECRET")
37+ if not secret:
38+ return None
39+ return hmac.new(secret.encode(),
40+ f"{CLIENT_ID}.{ka_id}.{ts}".encode(),
41+ hashlib.sha256).hexdigest()
42+
43+
44+def linked(ka_id: str | None) -> bool:
45+ """Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe)."""
46+ return bool(ka_id) and str(ka_id).startswith("ka-")
47+
48+
49+def clean_item(item: dict) -> dict:
50+ """Ne garde que les champs d'item connus, en chaînes tronquées."""
51+ return {k: str(item.get(k) or "")[:n]
52+ for k, n in _FIELDS.items() if item.get(k)}
53+
54+
55+def hub_toggle(ka_id: str, action: str, item: dict) -> bool:
56+ """Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :
57+ le hub est le magasin des favoris, l'échec doit remonter à l'appelant."""
58+ ts = int(time.time())
59+ sig = _sig(ka_id, ts)
60+ if not sig or not linked(ka_id) or action not in ("add", "remove"):
61+ return False
62+ try:
63+ r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,
64+ json={"client_id": CLIENT_ID, "ka_id": ka_id,
65+ "ts": str(ts), "sig": sig,
66+ "action": action, "item": item})
67+ ok = r.status_code == 200
68+ except Exception:
69+ ok = False
70+ if ok:
71+ with _lock:
72+ _cache.pop(ka_id, None) # la prochaine lecture reflète le toggle
73+ return ok
74+
75+
76+def hub_list(ka_id: str) -> list[dict] | None:
77+ """Favoris Créa-Ka du membre, lus au hub (cache mémoire 30 s).
78+ [] = aucun favori ; None = hub injoignable (erreur, jamais mise en cache)."""
79+ if not linked(ka_id):
80+ return [] # compte legacy non relié au hub
81+ now = time.time()
82+ with _lock:
83+ hit = _cache.get(ka_id)
84+ if hit and now - hit[0] < CACHE_TTL:
85+ return hit[1]
86+ sig = _sig(ka_id, int(now))
87+ if not sig:
88+ return None
89+ try:
90+ r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,
91+ params={"client_id": CLIENT_ID, "ka_id": ka_id,
92+ "ts": int(now), "sig": sig})
93+ if r.status_code != 200:
94+ return None
95+ favs = r.json().get("favorites") or []
96+ if not isinstance(favs, list):
97+ return None
98+ except Exception:
99+ return None
100+ favs = [f for f in favs if isinstance(f, dict)]
101+ with _lock:
102+ _cache[ka_id] = (now, favs)
103+ return favs
modified creaka/web.py +36 −2
@@ -10,13 +10,13 @@ import json
10 import threading 10 import threading
11 from pathlib import Path 11 from pathlib import Path
12 12
13−from fastapi import Body, FastAPI, HTTPException, Query 13+from fastapi import Body, FastAPI, HTTPException, Query, Request
14 from fastapi.middleware.cors import CORSMiddleware 14 from fastapi.middleware.cors import CORSMiddleware
15 from fastapi.middleware.gzip import GZipMiddleware 15 from fastapi.middleware.gzip import GZipMiddleware
16 from fastapi.responses import FileResponse, Response 16 from fastapi.responses import FileResponse, Response
17 from pydantic import BaseModel, Field 17 from pydantic import BaseModel, Field
18 18
19−from . import auth, db, ethics, kapdf, seo 19+from . import auth, db, ethics, hubfav, kapdf, seo
20 from . import stats as stats_mod 20 from . import stats as stats_mod
21 from .normalize import NICHES, PLATFORMS, REGIONS 21 from .normalize import NICHES, PLATFORMS, REGIONS
22 22
@@ -203,6 +203,40 @@ def optout(req: OptOutRequest):
203 "message": "Fiche masquée. La demande sera vérifiée ; merci."} 203 "message": "Fiche masquée. La demande sera vérifiée ; merci."}
204 204
205 205
206+# --- Favoris ♥ « Mon univers Ka » (magasin central : hub groupe-ka.com) -----
207+
208+@app.get("/api/favorites")
209+def favorites(request: Request):
210+ """Favoris du membre connecté, lus au hub Groupe KA (aucun stockage local)."""
211+ user = auth.current_user(request)
212+ if not user:
213+ raise HTTPException(401, "Connexion KA ID requise")
214+ items = hubfav.hub_list(user.get("ka_id") or "")
215+ if items is None:
216+ raise HTTPException(502, "Hub Groupe KA injoignable — réessayez")
217+ return {"ids": [i["item_id"] for i in items if i.get("item_id")],
218+ "items": items}
219+
220+
221+@app.post("/api/favorites/toggle")
222+def toggle_favorite(request: Request, body: dict = Body(...)):
223+ """Ajoute (on=true) ou retire (on=false) un favori — poussé au hub
224+ Groupe KA de façon synchrone : le hub est la seule source de vérité."""
225+ user = auth.current_user(request)
226+ if not user:
227+ raise HTTPException(401, "Connexion KA ID requise")
228+ ka_id = user.get("ka_id") or ""
229+ if not hubfav.linked(ka_id):
230+ raise HTTPException(403, "Compte non relié au hub Groupe KA")
231+ on = bool(body.get("on"))
232+ item = hubfav.clean_item(body.get("item") or {})
233+ if not item.get("item_id"):
234+ raise HTTPException(422, "item.item_id requis")
235+ if not hubfav.hub_toggle(ka_id, "add" if on else "remove", item):
236+ raise HTTPException(502, "Hub Groupe KA injoignable — favori non enregistré")
237+ return {"ok": True, "on": on}
238+
239+
206 # SEO : fiches créateur SSR, robots.txt, sitemaps — déclaré AVANT 240 # SEO : fiches créateur SSR, robots.txt, sitemaps — déclaré AVANT
207 # les routes statiques pour prendre la main sur le fallback SPA 241 # les routes statiques pour prendre la main sur le fallback SPA
208 app.include_router(seo.router) 242 app.include_router(seo.router)
modified frontend/dist/index.html +171 −5
@@ -330,7 +330,7 @@ html.ka-scroll-lock, html.ka-scroll-lock body { overflow: hidden !important; ove
330 / Inter / JetBrains Mono) — accent attribué à Créa-Ka : VIOLET 330 / Inter / JetBrains Mono) — accent attribué à Créa-Ka : VIOLET
331 (--accent:#7048e8). Patrons mobiles calqués sur Lou-Ka (tab bar 331 (--accent:#7048e8). Patrons mobiles calqués sur Lou-Ka (tab bar
332 4 onglets, filtres défilants, anti-zoom iOS). 332 4 onglets, filtres défilants, anti-zoom iOS).
333− Routes : / · /createur/{id} · /stats · /retrait · /compte · /contact 333+ Routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact
334 --> 334 -->
335 <!DOCTYPE html> 335 <!DOCTYPE html>
336 <html lang="fr-CA"> 336 <html lang="fr-CA">
@@ -871,6 +871,34 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
871 box-shadow:2px 2px 0 rgba(20,24,20,.25)} 871 box-shadow:2px 2px 0 rgba(20,24,20,.25)}
872 .plat svg{width:15px;height:15px;fill:currentColor} 872 .plat svg{width:15px;height:15px;fill:currentColor}
873 .plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)} 873 .plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)}
874+/* ===== favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ===== */
875+.favbtn{position:absolute;right:10px;bottom:10px;z-index:2;width:36px;height:36px;
876+ border-radius:50%;border:1.5px solid var(--ink);background:var(--surface);
877+ color:var(--ink-2);display:flex;align-items:center;justify-content:center;
878+ padding:0;box-shadow:2px 2px 0 rgba(20,24,20,.25);transition:.15s}
879+.favbtn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;
880+ stroke-linecap:round;stroke-linejoin:round}
881+.favbtn:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink);color:var(--accent-deep)}
882+.favbtn:active{transform:translate(1px,1px);box-shadow:none}
883+.favbtn.on{background:var(--accent);color:var(--on-accent)}
884+.favbtn.on svg{fill:currentColor}
885+.favbtn[disabled]{opacity:.55;pointer-events:none}
886+.fav-cta{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);
887+ background:var(--surface);color:var(--ink);border-radius:var(--r-ctl);
888+ padding:10px 18px;min-height:44px;font-family:var(--font-display);font-size:13px;
889+ font-weight:700;transition:.15s;box-shadow:3px 3px 0 rgba(20,24,20,.25);cursor:pointer}
890+.fav-cta svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;
891+ stroke-linecap:round;stroke-linejoin:round;flex:none}
892+.fav-cta:hover{background:var(--accent);color:var(--on-accent)}
893+.fav-cta:active{transform:translate(2px,2px);box-shadow:none}
894+.fav-cta.on{background:var(--accent);color:var(--on-accent)}
895+.fav-cta.on svg{fill:currentColor}
896+.fav-cta[disabled]{opacity:.55;pointer-events:none}
897+.fav-empty{padding:50px 0;text-align:center;color:var(--ink-3)}
898+.fav-empty h2{text-transform:uppercase;color:var(--ink);margin-bottom:10px}
899+.favoris-page{padding:40px 0 60px}
900+.favoris-page .kicker+h1{font-size:clamp(26px,4.4vw,42px);margin:12px 0 0;text-transform:uppercase}
901+.favoris-page .lead{color:var(--ink-2);margin:14px 0 0;max-width:620px}
874 .more-wrap{display:flex;justify-content:center;padding:16px 0 44px} 902 .more-wrap{display:flex;justify-content:center;padding:16px 0 44px}
875 .empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1} 903 .empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1}
876 .count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase; 904 .count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;
@@ -1233,7 +1261,7 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
1233 @media(max-width:480px){.site-item span{display:none}} 1261 @media(max-width:480px){.site-item span{display:none}}
1234 .tabbar{display:none} 1262 .tabbar{display:none}
1235 @media(max-width:940px){ 1263 @media(max-width:940px){
1236− .tabbar{display:grid;grid-template-columns:repeat(4,1fr); 1264+ .tabbar{display:grid;grid-template-columns:repeat(5,1fr);
1237 position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600); 1265 position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600);
1238 background:rgba(245,243,238,.94); 1266 background:rgba(245,243,238,.94);
1239 backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px); 1267 backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
@@ -1259,7 +1287,7 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
1259 <div id="app"></div> 1287 <div id="app"></div>
1260 <script> 1288 <script>
1261 "use strict"; 1289 "use strict";
1262−/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /retrait · /compte · /contact */ 1290+/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact */
1263 const ICONS={"_author":"Simon-Pierre Boucher <contact@spboucher.ai>","_file":"frontend/src/icons.json","_desc":"Trac\u00e9s SVG des marques de plateformes (simple-icons v13, CC0) + glyphes maison (linkedin/site-web/autre)","instagram":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077","tiktok":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z","youtube":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z","twitch":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z","kick":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z","x":"M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z","facebook":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z","snapchat":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z","substack":"M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z","patreon":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z","onlyfans":"M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173 0 0-2.399.585-4.43 0 4.018-.777 6.333-3.037 7.005-5.995zM5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996 7.996 7.996 0 0 0-7.996-7.996Zm0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z","threads":"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z","podcast":"M5.34 0A5.328 5.328 0 000 5.34v13.32A5.328 5.328 0 005.34 24h13.32A5.328 5.328 0 0024 18.66V5.34A5.328 5.328 0 0018.66 0zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 01-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.536-.224 1.448-.874 2.008-1.435a7.644 7.644 0 002.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.696-2.928-4.792-5.624-5.352-.784-.16-2.208-.16-3 0-2.728.56-4.984 2.76-5.672 5.528-.184.752-.184 2.584 0 3.336.456 1.832 1.64 3.512 3.192 4.512.304.2.672.408.824.472.336.144.408.264.472.856.04.36.03.464-.056.464-.056 0-.464-.176-.896-.384l-.04-.03c-2.472-1.216-4.056-3.274-4.632-6.012-.144-.706-.168-2.392-.03-3.04.36-1.74 1.048-3.1 2.192-4.304 1.648-1.737 3.768-2.656 6.128-2.656zm.134 2.81c.409.004.803.04 1.106.106 2.784.62 4.76 3.408 4.376 6.174-.152 1.114-.536 2.03-1.216 2.88-.336.43-1.152 1.15-1.296 1.15-.023 0-.048-.272-.048-.603v-.605l.416-.496c1.568-1.878 1.456-4.502-.256-6.224-.664-.67-1.432-1.064-2.424-1.246-.64-.118-.776-.118-1.448-.008-1.02.167-1.81.562-2.512 1.256-1.72 1.704-1.832 4.342-.264 6.222l.413.496v.608c0 .336-.027.608-.06.608-.03 0-.264-.16-.512-.36l-.034-.011c-.832-.664-1.568-1.842-1.872-2.997-.184-.698-.184-2.024.008-2.72.504-1.878 1.888-3.335 3.808-4.019.41-.145 1.133-.22 1.814-.211zm-.13 2.99c.31 0 .62.06.844.178.488.253.888.745 1.04 1.259.464 1.578-1.208 2.96-2.72 2.254h-.015c-.712-.331-1.096-.956-1.104-1.77 0-.733.408-1.371 1.112-1.745.224-.117.534-.176.844-.176zm-.011 4.728c.988-.004 1.706.349 1.97.97.198.464.124 1.932-.218 4.302-.232 1.656-.36 2.074-.68 2.356-.44.39-1.064.498-1.656.288h-.003c-.716-.257-.87-.605-1.164-2.644-.341-2.37-.416-3.838-.218-4.302.262-.616.974-.966 1.97-.97z","linktree":"m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z","linkedin":"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z","site-web":"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm7.938 9h-3.243a15.6 15.6 0 00-1.4-4.653A8.03 8.03 0 0119.938 9zM12 2.05c.9 1.2 1.9 3.2 2.5 6.95h-5C10.1 5.25 11.1 3.25 12 2.05zM4.062 15A8.06 8.06 0 013.75 12c0-1.05.15-2.05.312-3h3.55A25 25 0 007.5 12c0 1.05.05 2.05.113 3zm.643 2h3.243c.35 1.8.85 3.35 1.4 4.653A8.03 8.03 0 014.705 17zm3.243-10H4.705a8.03 8.03 0 014.643-4.653A15.6 15.6 0 007.948 7zM12 21.95c-.9-1.2-1.9-3.2-2.5-6.95h5c-.6 3.75-1.6 5.75-2.5 6.95zM14.787 13H9.213A23 23 0 019.1 12c0-1.05.037-2.05.113-3h5.574c.076.95.113 1.95.113 3s-.037 2.05-.113 3zm.508 8.653c.55-1.303 1.05-2.853 1.4-4.653h3.243a8.03 8.03 0 01-4.643 4.653zM16.388 15c.062-.95.112-1.95.112-3s-.05-2.05-.112-3h3.55c.162.95.312 1.95.312 3s-.15 2.05-.312 3z","autre":"M10.59 13.41a1 1 0 010-1.41l2.83-2.83a3 3 0 114.24 4.24l-1.41 1.42a1 1 0 11-1.42-1.42l1.42-1.41a1 1 0 10-1.42-1.42L12 13.41a1 1 0 01-1.41 0zm2.82-2.82a1 1 0 010 1.41l-2.82 2.83a3 3 0 11-4.25-4.24l1.42-1.42a1 1 0 111.41 1.42L7.76 12a1 1 0 101.41 1.41L12 10.59a1 1 0 011.41 0z","spotify":"M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z","discord":"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"}; 1291 const ICONS={"_author":"Simon-Pierre Boucher <contact@spboucher.ai>","_file":"frontend/src/icons.json","_desc":"Trac\u00e9s SVG des marques de plateformes (simple-icons v13, CC0) + glyphes maison (linkedin/site-web/autre)","instagram":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077","tiktok":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z","youtube":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z","twitch":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z","kick":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z","x":"M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z","facebook":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z","snapchat":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z","substack":"M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z","patreon":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z","onlyfans":"M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173 0 0-2.399.585-4.43 0 4.018-.777 6.333-3.037 7.005-5.995zM5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996 7.996 7.996 0 0 0-7.996-7.996Zm0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z","threads":"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z","podcast":"M5.34 0A5.328 5.328 0 000 5.34v13.32A5.328 5.328 0 005.34 24h13.32A5.328 5.328 0 0024 18.66V5.34A5.328 5.328 0 0018.66 0zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 01-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.536-.224 1.448-.874 2.008-1.435a7.644 7.644 0 002.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.696-2.928-4.792-5.624-5.352-.784-.16-2.208-.16-3 0-2.728.56-4.984 2.76-5.672 5.528-.184.752-.184 2.584 0 3.336.456 1.832 1.64 3.512 3.192 4.512.304.2.672.408.824.472.336.144.408.264.472.856.04.36.03.464-.056.464-.056 0-.464-.176-.896-.384l-.04-.03c-2.472-1.216-4.056-3.274-4.632-6.012-.144-.706-.168-2.392-.03-3.04.36-1.74 1.048-3.1 2.192-4.304 1.648-1.737 3.768-2.656 6.128-2.656zm.134 2.81c.409.004.803.04 1.106.106 2.784.62 4.76 3.408 4.376 6.174-.152 1.114-.536 2.03-1.216 2.88-.336.43-1.152 1.15-1.296 1.15-.023 0-.048-.272-.048-.603v-.605l.416-.496c1.568-1.878 1.456-4.502-.256-6.224-.664-.67-1.432-1.064-2.424-1.246-.64-.118-.776-.118-1.448-.008-1.02.167-1.81.562-2.512 1.256-1.72 1.704-1.832 4.342-.264 6.222l.413.496v.608c0 .336-.027.608-.06.608-.03 0-.264-.16-.512-.36l-.034-.011c-.832-.664-1.568-1.842-1.872-2.997-.184-.698-.184-2.024.008-2.72.504-1.878 1.888-3.335 3.808-4.019.41-.145 1.133-.22 1.814-.211zm-.13 2.99c.31 0 .62.06.844.178.488.253.888.745 1.04 1.259.464 1.578-1.208 2.96-2.72 2.254h-.015c-.712-.331-1.096-.956-1.104-1.77 0-.733.408-1.371 1.112-1.745.224-.117.534-.176.844-.176zm-.011 4.728c.988-.004 1.706.349 1.97.97.198.464.124 1.932-.218 4.302-.232 1.656-.36 2.074-.68 2.356-.44.39-1.064.498-1.656.288h-.003c-.716-.257-.87-.605-1.164-2.644-.341-2.37-.416-3.838-.218-4.302.262-.616.974-.966 1.97-.97z","linktree":"m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z","linkedin":"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z","site-web":"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm7.938 9h-3.243a15.6 15.6 0 00-1.4-4.653A8.03 8.03 0 0119.938 9zM12 2.05c.9 1.2 1.9 3.2 2.5 6.95h-5C10.1 5.25 11.1 3.25 12 2.05zM4.062 15A8.06 8.06 0 013.75 12c0-1.05.15-2.05.312-3h3.55A25 25 0 007.5 12c0 1.05.05 2.05.113 3zm.643 2h3.243c.35 1.8.85 3.35 1.4 4.653A8.03 8.03 0 014.705 17zm3.243-10H4.705a8.03 8.03 0 014.643-4.653A15.6 15.6 0 007.948 7zM12 21.95c-.9-1.2-1.9-3.2-2.5-6.95h5c-.6 3.75-1.6 5.75-2.5 6.95zM14.787 13H9.213A23 23 0 019.1 12c0-1.05.037-2.05.113-3h5.574c.076.95.113 1.95.113 3s-.037 2.05-.113 3zm.508 8.653c.55-1.303 1.05-2.853 1.4-4.653h3.243a8.03 8.03 0 01-4.643 4.653zM16.388 15c.062-.95.112-1.95.112-3s-.05-2.05-.112-3h3.55c.162.95.312 1.95.312 3s-.15 2.05-.312 3z","autre":"M10.59 13.41a1 1 0 010-1.41l2.83-2.83a3 3 0 114.24 4.24l-1.41 1.42a1 1 0 11-1.42-1.42l1.42-1.41a1 1 0 10-1.42-1.42L12 13.41a1 1 0 01-1.41 0zm2.82-2.82a1 1 0 010 1.41l-2.82 2.83a3 3 0 11-4.25-4.24l1.42-1.42a1 1 0 111.41 1.42L7.76 12a1 1 0 101.41 1.41L12 10.59a1 1 0 011.41 0z","spotify":"M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z","discord":"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"};
1264 /* écosystème Groupe KA (ka/ecosystem.json, injecté au build) — footer commun + /contact */ 1292 /* écosystème Groupe KA (ka/ecosystem.json, injecté au build) — footer commun + /contact */
1265 const KA_ECO={"org":{"name":"Groupe KA","legalName":"Groupe KA — Simon-Pierre Boucher","tagline":"Holding québécois d'agrégateurs de produits et services entièrement automatisés.","disclaimer":"Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.","copyrightHolder":"Groupe KA — Simon-Pierre Boucher"},"hub":{"url":"https://www.groupe-ka.com","loginPath":"/connexion","signupNote":"La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login."},"contacts":[{"email":"contact@groupe-ka.com","role":"Projets, partenariats & données"},{"email":"info@groupe-ka.com","role":"Médias & questions générales"},{"email":"admin@groupe-ka.com","role":"Légal, vie privée & Loi 25"}],"legal":[{"label":"Conditions d'utilisation","href":"https://www.groupe-ka.com/conditions"},{"label":"Politique de confidentialité","href":"https://www.groupe-ka.com/confidentialite"},{"label":"Protection des renseignements personnels (Loi 25)","href":"https://www.groupe-ka.com/loi-25"},{"label":"Transparence des robots d'indexation","href":"https://www.groupe-ka.com/bots"}],"sites":[{"id":"groupe-ka","wordmark":"Groupe KA","domain":"www.groupe-ka.com","accent":"#d9f26b","accentSoft":"#f0f9d2","accentDeep":"#123f2e","onAccent":"#141814","tagline":"Le portail de l'écosystème ·Ka"},{"id":"trouve-ka","wordmark":"Trouve·Ka","domain":"www.trouve-ka.com","accent":"#1c7ed6","accentSoft":"#e7f2fd","accentDeep":"#14508f","onAccent":"#ffffff","tagline":"Le moteur de recherche du web québécois"},{"id":"lou-ka","wordmark":"Lou·Ka","domain":"www.lou-ka.com","accent":"#ff6a00","accentSoft":"#fff1e6","accentDeep":"#cc5500","onAccent":"#ffffff","tagline":"Tous les logements à louer"},{"id":"immo-ka","wordmark":"Immo·Ka","domain":"www.immo-ka.com","accent":"#e23744","accentSoft":"#fbe0e2","accentDeep":"#a8232e","onAccent":"#ffffff","tagline":"Toutes les propriétés à vendre"},{"id":"vrai-prix","wordmark":"Vrai-Prix","domain":"www.vrai-prix.com","accent":"#ff5148","accentSoft":"#ffe3e0","accentDeep":"#9e2a25","onAccent":"#ffffff","tagline":"La valeur réelle de chaque propriété"},{"id":"auto-ka","wordmark":"Auto·Ka","domain":"www.auto-ka.com","accent":"#ff5a2a","accentSoft":"#ffe8de","accentDeep":"#cc3f16","onAccent":"#ffffff","tagline":"Les voitures usagées du Québec"},{"id":"fabri-ka","wordmark":"Fabri·Ka","domain":"www.fabri-ka.com","accent":"#c4532e","accentSoft":"#f7e3da","accentDeep":"#a94525","onAccent":"#ffffff","tagline":"Les produits fabriqués au Québec"},{"id":"food-ka","wordmark":"Food·Ka","domain":"www.food-ka.com","accent":"#1f9d55","accentSoft":"#e2f5ea","accentDeep":"#157a40","onAccent":"#ffffff","tagline":"Les prix d'épicerie, suivis à la source"},{"id":"resto-ka","wordmark":"Resto·Ka","domain":"www.resto-ka.com","accent":"#f08c00","accentSoft":"#fdeed7","accentDeep":"#b96a00","onAccent":"#141814","tagline":"Chaque resto, chaque plat, chaque prix"},{"id":"sorti-ka","wordmark":"Sorti·Ka","domain":"www.sorti-ka.com","accent":"#d6336c","accentSoft":"#fbe0eb","accentDeep":"#a12551","onAccent":"#ffffff","tagline":"Toutes les sorties, dans les 17 régions"},{"id":"crea-ka","wordmark":"Créa·Ka","domain":"www.crea-ka.com","accent":"#7048e8","accentSoft":"#ece5fc","accentDeep":"#5433b8","onAccent":"#ffffff","tagline":"Les créateurs d'ici, tous leurs liens"},{"id":"api-ka","wordmark":"API·Ka","domain":"www.api-ka.com","accent":"#3b5bdb","accentSoft":"#e4eafb","accentDeep":"#2b44a8","onAccent":"#ffffff","tagline":"La donnée de l'écosystème, par API"},{"id":"job-ka","wordmark":"Job·Ka","domain":"www.job-ka.com","accent":"#0c8599","accentSoft":"#def0f4","accentDeep":"#095c6b","onAccent":"#ffffff","tagline":"Tous les emplois des employeurs québécois"}],"extraFooterLinks":[{"label":"ValoPlex","href":"https://www.valoplex.com"},{"label":"Ka2","href":"https://www.ka2.bot"},{"label":"Ka4","href":"https://www.ka4.bot"},{"label":"Ka6","href":"https://www.ka6.bot"}]}; 1293 const KA_ECO={"org":{"name":"Groupe KA","legalName":"Groupe KA — Simon-Pierre Boucher","tagline":"Holding québécois d'agrégateurs de produits et services entièrement automatisés.","disclaimer":"Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.","copyrightHolder":"Groupe KA — Simon-Pierre Boucher"},"hub":{"url":"https://www.groupe-ka.com","loginPath":"/connexion","signupNote":"La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login."},"contacts":[{"email":"contact@groupe-ka.com","role":"Projets, partenariats & données"},{"email":"info@groupe-ka.com","role":"Médias & questions générales"},{"email":"admin@groupe-ka.com","role":"Légal, vie privée & Loi 25"}],"legal":[{"label":"Conditions d'utilisation","href":"https://www.groupe-ka.com/conditions"},{"label":"Politique de confidentialité","href":"https://www.groupe-ka.com/confidentialite"},{"label":"Protection des renseignements personnels (Loi 25)","href":"https://www.groupe-ka.com/loi-25"},{"label":"Transparence des robots d'indexation","href":"https://www.groupe-ka.com/bots"}],"sites":[{"id":"groupe-ka","wordmark":"Groupe KA","domain":"www.groupe-ka.com","accent":"#d9f26b","accentSoft":"#f0f9d2","accentDeep":"#123f2e","onAccent":"#141814","tagline":"Le portail de l'écosystème ·Ka"},{"id":"trouve-ka","wordmark":"Trouve·Ka","domain":"www.trouve-ka.com","accent":"#1c7ed6","accentSoft":"#e7f2fd","accentDeep":"#14508f","onAccent":"#ffffff","tagline":"Le moteur de recherche du web québécois"},{"id":"lou-ka","wordmark":"Lou·Ka","domain":"www.lou-ka.com","accent":"#ff6a00","accentSoft":"#fff1e6","accentDeep":"#cc5500","onAccent":"#ffffff","tagline":"Tous les logements à louer"},{"id":"immo-ka","wordmark":"Immo·Ka","domain":"www.immo-ka.com","accent":"#e23744","accentSoft":"#fbe0e2","accentDeep":"#a8232e","onAccent":"#ffffff","tagline":"Toutes les propriétés à vendre"},{"id":"vrai-prix","wordmark":"Vrai-Prix","domain":"www.vrai-prix.com","accent":"#ff5148","accentSoft":"#ffe3e0","accentDeep":"#9e2a25","onAccent":"#ffffff","tagline":"La valeur réelle de chaque propriété"},{"id":"auto-ka","wordmark":"Auto·Ka","domain":"www.auto-ka.com","accent":"#ff5a2a","accentSoft":"#ffe8de","accentDeep":"#cc3f16","onAccent":"#ffffff","tagline":"Les voitures usagées du Québec"},{"id":"fabri-ka","wordmark":"Fabri·Ka","domain":"www.fabri-ka.com","accent":"#c4532e","accentSoft":"#f7e3da","accentDeep":"#a94525","onAccent":"#ffffff","tagline":"Les produits fabriqués au Québec"},{"id":"food-ka","wordmark":"Food·Ka","domain":"www.food-ka.com","accent":"#1f9d55","accentSoft":"#e2f5ea","accentDeep":"#157a40","onAccent":"#ffffff","tagline":"Les prix d'épicerie, suivis à la source"},{"id":"resto-ka","wordmark":"Resto·Ka","domain":"www.resto-ka.com","accent":"#f08c00","accentSoft":"#fdeed7","accentDeep":"#b96a00","onAccent":"#141814","tagline":"Chaque resto, chaque plat, chaque prix"},{"id":"sorti-ka","wordmark":"Sorti·Ka","domain":"www.sorti-ka.com","accent":"#d6336c","accentSoft":"#fbe0eb","accentDeep":"#a12551","onAccent":"#ffffff","tagline":"Toutes les sorties, dans les 17 régions"},{"id":"crea-ka","wordmark":"Créa·Ka","domain":"www.crea-ka.com","accent":"#7048e8","accentSoft":"#ece5fc","accentDeep":"#5433b8","onAccent":"#ffffff","tagline":"Les créateurs d'ici, tous leurs liens"},{"id":"api-ka","wordmark":"API·Ka","domain":"www.api-ka.com","accent":"#3b5bdb","accentSoft":"#e4eafb","accentDeep":"#2b44a8","onAccent":"#ffffff","tagline":"La donnée de l'écosystème, par API"},{"id":"job-ka","wordmark":"Job·Ka","domain":"www.job-ka.com","accent":"#0c8599","accentSoft":"#def0f4","accentDeep":"#095c6b","onAccent":"#ffffff","tagline":"Tous les emplois des employeurs québécois"}],"extraFooterLinks":[{"label":"ValoPlex","href":"https://www.valoplex.com"},{"label":"Ka2","href":"https://www.ka2.bot"},{"label":"Ka4","href":"https://www.ka4.bot"},{"label":"Ka6","href":"https://www.ka6.bot"}]};
@@ -1305,6 +1333,63 @@ async function api(p){const r=await fetch(p);if(!r.ok)throw new Error(r.status);
1305 function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`} 1333 function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`}
1306 const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`; 1334 const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`;
1307 1335
1336+/* ---------- favoris ♥ « Mon univers Ka » — magasin central : hub groupe-ka.com
1337+ (AUCUN stockage local : toggle synchrone via /api/favorites/toggle, lecture
1338+ /api/favorites — cache serveur 30 s). item_id = id stable du créateur. ---------- */
1339+const heartSvg=`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`;
1340+const FAV={ids:new Set(),loaded:false,src:{}};
1341+function favItem(c){
1342+ const main=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];
1343+ const sub=main?(PLAT[main.platform]||PLAT.autre).label:(c.region||"");
1344+ return {item_id:c.id,title:c.display_name||c.id,
1345+ subtitle:sub,image_url:avatarSources(c)[0]||"",
1346+ url:"https://www.crea-ka.com/createur/"+encodeURIComponent(c.id)};
1347+}
1348+function favBtn(c){
1349+ FAV.src[c.id]=favItem(c);
1350+ const on=FAV.ids.has(c.id);
1351+ return `<button type="button" class="favbtn${on?" on":""}" data-fav="${esc(c.id)}"
1352+ aria-pressed="${on}" aria-label="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"
1353+ title="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"
1354+ onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button>`;
1355+}
1356+function favPaint(){
1357+ document.querySelectorAll("[data-fav]").forEach(b=>{
1358+ const on=FAV.ids.has(b.dataset.fav);
1359+ b.classList.toggle("on",on);b.setAttribute("aria-pressed",on);
1360+ const lbl=on?"Retirer de mes favoris":"Ajouter à mes favoris";
1361+ b.title=lbl;b.setAttribute("aria-label",lbl);
1362+ const t=b.querySelector(".fav-cta-txt");if(t)t.textContent=lbl;
1363+ });
1364+}
1365+async function favLoad(){
1366+ if(FAV.loaded||!state.me)return favPaint();
1367+ try{const r=await fetch("/api/favorites");
1368+ if(r.ok){const j=await r.json();FAV.ids=new Set(j.ids||[]);FAV.loaded=true;}
1369+ }catch(e){}
1370+ favPaint();
1371+}
1372+window.favToggle=async(id,el)=>{
1373+ if(state.me===null){location.href="/api/auth/ka/login";return}
1374+ const on=!FAV.ids.has(id);
1375+ if(el)el.disabled=true;
1376+ try{
1377+ const r=await fetch("/api/favorites/toggle",{method:"POST",
1378+ headers:{"Content-Type":"application/json"},
1379+ body:JSON.stringify({on,item:FAV.src[id]||{item_id:id}})});
1380+ if(r.status===401){location.href="/api/auth/ka/login";return}
1381+ if(!r.ok)throw new Error(r.status);
1382+ if(on)FAV.ids.add(id);else FAV.ids.delete(id);
1383+ if(!on&&location.pathname==="/favoris"&&el){
1384+ const card=el.closest(".card");if(card)card.remove();
1385+ const grid=$("#fav-grid");
1386+ if(grid&&!grid.querySelector(".card"))renderFavoris();
1387+ }
1388+ }catch(e){alert("Impossible de synchroniser le favori avec le hub Groupe KA — réessayez.")}
1389+ if(el)el.disabled=false;
1390+ favPaint();
1391+};
1392+
1308 /* cascade de photos : avatar capté à la source, puis unavatar par plateforme, 1393 /* cascade de photos : avatar capté à la source, puis unavatar par plateforme,
1309 repli final = initiales sur motif encre/violet (déjà en dessous) */ 1394 repli final = initiales sur motif encre/violet (déjà en dessous) */
1310 function avatarSources(c){ 1395 function avatarSources(c){
@@ -1338,6 +1423,7 @@ function header(){
1338 <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span> 1423 <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span>
1339 <span class="header-spacer"></span> 1424 <span class="header-spacer"></span>
1340 <a class="hbtn" href="/stats" data-nav>Statistiques</a> 1425 <a class="hbtn" href="/stats" data-nav>Statistiques</a>
1426+ <a class="hbtn" href="/favoris" data-nav>♥ Favoris</a>
1341 <a class="hbtn" href="/contact" data-nav>Contact</a> 1427 <a class="hbtn" href="/contact" data-nav>Contact</a>
1342 <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a> 1428 <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a>
1343 <span id="acct-slot"></span> 1429 <span id="acct-slot"></span>
@@ -1359,12 +1445,29 @@ async function loadAccount(){
1359 title="Se connecter avec KA ID"><span class="acct-long">Se connecter avec</span><span class="acct-short">Connexion</span> <span class="ka-badge">KA</span></a>`; 1445 title="Se connecter avec KA ID"><span class="acct-long">Se connecter avec</span><span class="acct-short">Connexion</span> <span class="ka-badge">KA</span></a>`;
1360 } 1446 }
1361 bindNav(slot); 1447 bindNav(slot);
1448+ if(me){favSlotPaint();favLoad();}
1449+ else{FAV.ids=new Set();FAV.loaded=false;favSlotPaint();favPaint();}
1450+}
1451+/* CTA favori de la fiche créateur : ♥ si connecté, sinon « Se connecter avec KA ID » */
1452+function favSlotPaint(){
1453+ const s=document.getElementById("fav-slot");if(!s)return;
1454+ const c=state.creator;if(!c)return;
1455+ if(state.me){
1456+ FAV.src[c.id]=favItem(c);
1457+ const on=FAV.ids.has(c.id);
1458+ s.innerHTML=`<button type="button" class="fav-cta${on?" on":""}" data-fav="${esc(c.id)}"
1459+ aria-pressed="${on}" onclick="favToggle(this.dataset.fav,this)">${heartSvg}<span class="fav-cta-txt">${on?"Retirer de mes favoris":"Ajouter à mes favoris"}</span></button>`;
1460+ }else if(state.me===null){
1461+ s.innerHTML=`<a class="fav-cta" href="/api/auth/ka/login"
1462+ title="Un seul compte pour tout le Groupe KA — favoris partagés « Mon univers Ka »">${heartSvg}<span>Se connecter avec KA ID</span></a>`;
1463+ }
1362 } 1464 }
1363 function tabbar(active){ 1465 function tabbar(active){
1364 const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`; 1466 const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`;
1365 return `<nav class="tabbar"> 1467 return `<nav class="tabbar">
1366 ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")} 1468 ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")}
1367 ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")} 1469 ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")}
1470+ ${t("/favoris","Favoris",`<svg viewBox="0 0 24 24"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`,"fav")}
1368 ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")} 1471 ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")}
1369 ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")} 1472 ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")}
1370 </nav>`; 1473 </nav>`;
@@ -1378,6 +1481,7 @@ function footer(){
1378 return `<div class="footer-local"><div class="container"><nav> 1481 return `<div class="footer-local"><div class="container"><nav>
1379 <span class="klabel">Créa·Ka</span> 1482 <span class="klabel">Créa·Ka</span>
1380 <a href="/compte" data-nav>Mon compte KA ID</a> 1483 <a href="/compte" data-nav>Mon compte KA ID</a>
1484+ <a href="/favoris" data-nav>Mes favoris</a>
1381 <a href="/stats" data-nav>Statistiques</a> 1485 <a href="/stats" data-nav>Statistiques</a>
1382 <a href="/retrait" data-nav>Demander un retrait</a> 1486 <a href="/retrait" data-nav>Demander un retrait</a>
1383 <a href="/contact" data-nav>Contact</a> 1487 <a href="/contact" data-nav>Contact</a>
@@ -1421,7 +1525,7 @@ function cardHtml(c){
1421 return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer"> 1525 return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer">
1422 <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)} 1526 <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)}
1423 <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> 1527 <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
1424− <span class="nplat">${np} plat.</span></div> 1528+ <span class="nplat">${np} plat.</span>${favBtn(c)}</div>
1425 <div class="card-body"> 1529 <div class="card-body">
1426 <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div> 1530 <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div>
1427 <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div> 1531 <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div>
@@ -1661,6 +1765,7 @@ async function renderCreator(id){
1661 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))} 1765 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))}
1662 catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2> 1766 catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2>
1663 <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return} 1767 <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return}
1768+ state.creator=c;
1664 document.title=`${c.display_name} — Créa-Ka`; 1769 document.title=`${c.display_name} — Créa-Ka`;
1665 const verified=(c.platforms||[]).some(a=>a.verified); 1770 const verified=(c.platforms||[]).some(a=>a.verified);
1666 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join(""); 1771 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join("");
@@ -1727,7 +1832,8 @@ async function renderCreator(id){
1727 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> 1832 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
1728 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""} 1833 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""}
1729 ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div> 1834 ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div>
1730− ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}</div> 1835+ ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}
1836+ <div id="fav-slot" style="margin-top:16px"></div></div>
1731 </div> 1837 </div>
1732 <div class="hkpi-row">${heroKpis}</div> 1838 <div class="hkpi-row">${heroKpis}</div>
1733 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""} 1839 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""}
@@ -2268,6 +2374,65 @@ async function renderCompte(){
2268 state.me=null;history.pushState(null,"","/");route();}); 2374 state.me=null;history.pushState(null,"","/");route();});
2269 } 2375 }
2270 2376
2377+/* ---------- page favoris — « Mon univers Ka » (hub groupe-ka.com) ---------- */
2378+function favCardHtml(it){
2379+ const href=(it.url||"").replace(/^https?:\/\/(www\.)?crea-ka\.com/i,"")
2380+ ||("/createur/"+encodeURIComponent(it.item_id));
2381+ FAV.src[it.item_id]={item_id:it.item_id,title:it.title||"",subtitle:it.subtitle||"",
2382+ image_url:it.image_url||"",url:it.url||""};
2383+ return `<div class="card" data-href="${esc(href)}" role="link" tabindex="0" style="cursor:pointer">
2384+ <div class="card-photo"><span class="init">${esc(initials(it.title||"?"))}</span>
2385+ ${it.image_url?`<img loading="lazy" alt="${esc(it.title)}" src="${esc(it.image_url)}" onerror="this.remove()">`:""}
2386+ <button type="button" class="favbtn on" data-fav="${esc(it.item_id)}" aria-pressed="true"
2387+ aria-label="Retirer de mes favoris" title="Retirer de mes favoris"
2388+ onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button></div>
2389+ <div class="card-body">
2390+ <div class="card-name">${esc(it.title||it.item_id)}</div>
2391+ ${it.subtitle?`<div class="card-sub">${esc(it.subtitle)}</div>`:""}
2392+ </div></div>`;
2393+}
2394+async function renderFavoris(){
2395+ document.title="Mes favoris — Créa-Ka";
2396+ app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("fav");bindNav();
2397+ if(state.me===undefined){
2398+ try{const r=await fetch("/api/me");state.me=r.ok?await r.json():null}
2399+ catch(e){state.me=null}}
2400+ const shell=inner=>{app.innerHTML=header()+`
2401+ <div class="container favoris-page">
2402+ <span class="kicker">Mon univers Ka — favoris partagés</span>
2403+ <h1>Mes <span class="hl">favoris.</span></h1>
2404+ <p class="lead">Vos créateurs favoris, gardés dans votre compte KA ID sur groupe-ka.com :
2405+ les mêmes favoris vous suivent sur toutes les plateformes du Groupe KA.</p>
2406+ ${inner}
2407+ </div>`+footer()+tabbar("fav");bindNav();};
2408+ if(!state.me){
2409+ shell(`<div class="fav-empty" style="text-align:left;padding:34px 0">
2410+ <div class="compte-actions" style="margin-top:0">
2411+ <a class="btn btn-primary" href="/api/auth/ka/login">Se connecter avec KA ID</a>
2412+ <a class="btn btn-ghost" href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener">Créer un compte KA ID ↗</a>
2413+ </div>
2414+ <p style="font-size:12.5px;color:var(--ink-3);margin-top:16px">Un seul compte pour tout le
2415+ groupe — vos favoris sont entreposés au hub groupe-ka.com, jamais ici.</p></div>`);
2416+ loadAccount();return;
2417+ }
2418+ let data=null;
2419+ try{const r=await fetch("/api/favorites");if(r.ok)data=await r.json();}catch(e){}
2420+ if(!data){
2421+ shell(`<div class="fav-empty"><h2>Hub injoignable</h2>
2422+ <p>Impossible de lire vos favoris au hub Groupe KA pour le moment — réessayez dans un instant.</p></div>`);
2423+ loadAccount();return;
2424+ }
2425+ FAV.ids=new Set(data.ids||[]);FAV.loaded=true;
2426+ const items=(data.items||[]).filter(i=>i.item_id);
2427+ shell(items.length?`
2428+ <div class="count-line" style="padding-top:26px"><b>${fmtFull.format(items.length)}</b> créateur${items.length>1?"s":""} en favori</div>
2429+ <div class="grid" id="fav-grid">${items.map(favCardHtml).join("")}</div>`
2430+ :`<div class="fav-empty"><h2>Aucun favori pour l'instant</h2>
2431+ <p>Touchez le ♥ sur une carte ou une fiche de créateur pour l'ajouter à votre univers Ka.</p>
2432+ <p style="margin-top:16px"><a class="btn btn-primary" href="/#annuaire" data-nav>Explorer l'annuaire</a></p></div>`);
2433+ loadAccount();window.scrollTo(0,0);
2434+}
2435+
2271 /* ---------- page contact (écosystème Groupe KA — ka/ecosystem.json) ---------- */ 2436 /* ---------- page contact (écosystème Groupe KA — ka/ecosystem.json) ---------- */
2272 function renderContact(){ 2437 function renderContact(){
2273 document.title="Contact — Créa-Ka, un service Groupe KA"; 2438 document.title="Contact — Créa-Ka, un service Groupe KA";
@@ -2352,6 +2517,7 @@ function route(){
2352 if(m)r=renderCreator(decodeURIComponent(m[1])); 2517 if(m)r=renderCreator(decodeURIComponent(m[1]));
2353 else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();} 2518 else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();}
2354 else if(p==="/retrait")r=renderOptout(); 2519 else if(p==="/retrait")r=renderOptout();
2520+ else if(p==="/favoris")r=renderFavoris();
2355 else if(p==="/stats")r=renderStats(); 2521 else if(p==="/stats")r=renderStats();
2356 else if(p==="/compte")r=renderCompte(); 2522 else if(p==="/compte")r=renderCompte();
2357 else if(p==="/contact")r=renderContact(); 2523 else if(p==="/contact")r=renderContact();
modified frontend/src/index.template.html +171 −5
@@ -11,7 +11,7 @@
11 / Inter / JetBrains Mono) — accent attribué à Créa-Ka : VIOLET 11 / Inter / JetBrains Mono) — accent attribué à Créa-Ka : VIOLET
12 (--accent:#7048e8). Patrons mobiles calqués sur Lou-Ka (tab bar 12 (--accent:#7048e8). Patrons mobiles calqués sur Lou-Ka (tab bar
13 4 onglets, filtres défilants, anti-zoom iOS). 13 4 onglets, filtres défilants, anti-zoom iOS).
14− Routes : / · /createur/{id} · /stats · /retrait · /compte · /contact 14+ Routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact
15 --> 15 -->
16 <!DOCTYPE html> 16 <!DOCTYPE html>
17 <html lang="fr-CA"> 17 <html lang="fr-CA">
@@ -233,6 +233,34 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
233 box-shadow:2px 2px 0 rgba(20,24,20,.25)} 233 box-shadow:2px 2px 0 rgba(20,24,20,.25)}
234 .plat svg{width:15px;height:15px;fill:currentColor} 234 .plat svg{width:15px;height:15px;fill:currentColor}
235 .plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)} 235 .plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)}
236+/* ===== favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ===== */
237+.favbtn{position:absolute;right:10px;bottom:10px;z-index:2;width:36px;height:36px;
238+ border-radius:50%;border:1.5px solid var(--ink);background:var(--surface);
239+ color:var(--ink-2);display:flex;align-items:center;justify-content:center;
240+ padding:0;box-shadow:2px 2px 0 rgba(20,24,20,.25);transition:.15s}
241+.favbtn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;
242+ stroke-linecap:round;stroke-linejoin:round}
243+.favbtn:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink);color:var(--accent-deep)}
244+.favbtn:active{transform:translate(1px,1px);box-shadow:none}
245+.favbtn.on{background:var(--accent);color:var(--on-accent)}
246+.favbtn.on svg{fill:currentColor}
247+.favbtn[disabled]{opacity:.55;pointer-events:none}
248+.fav-cta{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);
249+ background:var(--surface);color:var(--ink);border-radius:var(--r-ctl);
250+ padding:10px 18px;min-height:44px;font-family:var(--font-display);font-size:13px;
251+ font-weight:700;transition:.15s;box-shadow:3px 3px 0 rgba(20,24,20,.25);cursor:pointer}
252+.fav-cta svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;
253+ stroke-linecap:round;stroke-linejoin:round;flex:none}
254+.fav-cta:hover{background:var(--accent);color:var(--on-accent)}
255+.fav-cta:active{transform:translate(2px,2px);box-shadow:none}
256+.fav-cta.on{background:var(--accent);color:var(--on-accent)}
257+.fav-cta.on svg{fill:currentColor}
258+.fav-cta[disabled]{opacity:.55;pointer-events:none}
259+.fav-empty{padding:50px 0;text-align:center;color:var(--ink-3)}
260+.fav-empty h2{text-transform:uppercase;color:var(--ink);margin-bottom:10px}
261+.favoris-page{padding:40px 0 60px}
262+.favoris-page .kicker+h1{font-size:clamp(26px,4.4vw,42px);margin:12px 0 0;text-transform:uppercase}
263+.favoris-page .lead{color:var(--ink-2);margin:14px 0 0;max-width:620px}
236 .more-wrap{display:flex;justify-content:center;padding:16px 0 44px} 264 .more-wrap{display:flex;justify-content:center;padding:16px 0 44px}
237 .empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1} 265 .empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1}
238 .count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase; 266 .count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;
@@ -595,7 +623,7 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
595 @media(max-width:480px){.site-item span{display:none}} 623 @media(max-width:480px){.site-item span{display:none}}
596 .tabbar{display:none} 624 .tabbar{display:none}
597 @media(max-width:940px){ 625 @media(max-width:940px){
598− .tabbar{display:grid;grid-template-columns:repeat(4,1fr); 626+ .tabbar{display:grid;grid-template-columns:repeat(5,1fr);
599 position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600); 627 position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600);
600 background:rgba(245,243,238,.94); 628 background:rgba(245,243,238,.94);
601 backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px); 629 backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
@@ -621,7 +649,7 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
621 <div id="app"></div> 649 <div id="app"></div>
622 <script> 650 <script>
623 "use strict"; 651 "use strict";
624−/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /retrait · /compte · /contact */ 652+/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact */
625 const ICONS=__ICONS__; 653 const ICONS=__ICONS__;
626 /* écosystème Groupe KA (ka/ecosystem.json, injecté au build) — footer commun + /contact */ 654 /* écosystème Groupe KA (ka/ecosystem.json, injecté au build) — footer commun + /contact */
627 const KA_ECO=__KA_ECO__; 655 const KA_ECO=__KA_ECO__;
@@ -667,6 +695,63 @@ async function api(p){const r=await fetch(p);if(!r.ok)throw new Error(r.status);
667 function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`} 695 function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`}
668 const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`; 696 const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`;
669 697
698+/* ---------- favoris ♥ « Mon univers Ka » — magasin central : hub groupe-ka.com
699+ (AUCUN stockage local : toggle synchrone via /api/favorites/toggle, lecture
700+ /api/favorites — cache serveur 30 s). item_id = id stable du créateur. ---------- */
701+const heartSvg=`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`;
702+const FAV={ids:new Set(),loaded:false,src:{}};
703+function favItem(c){
704+ const main=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];
705+ const sub=main?(PLAT[main.platform]||PLAT.autre).label:(c.region||"");
706+ return {item_id:c.id,title:c.display_name||c.id,
707+ subtitle:sub,image_url:avatarSources(c)[0]||"",
708+ url:"https://www.crea-ka.com/createur/"+encodeURIComponent(c.id)};
709+}
710+function favBtn(c){
711+ FAV.src[c.id]=favItem(c);
712+ const on=FAV.ids.has(c.id);
713+ return `<button type="button" class="favbtn${on?" on":""}" data-fav="${esc(c.id)}"
714+ aria-pressed="${on}" aria-label="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"
715+ title="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"
716+ onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button>`;
717+}
718+function favPaint(){
719+ document.querySelectorAll("[data-fav]").forEach(b=>{
720+ const on=FAV.ids.has(b.dataset.fav);
721+ b.classList.toggle("on",on);b.setAttribute("aria-pressed",on);
722+ const lbl=on?"Retirer de mes favoris":"Ajouter à mes favoris";
723+ b.title=lbl;b.setAttribute("aria-label",lbl);
724+ const t=b.querySelector(".fav-cta-txt");if(t)t.textContent=lbl;
725+ });
726+}
727+async function favLoad(){
728+ if(FAV.loaded||!state.me)return favPaint();
729+ try{const r=await fetch("/api/favorites");
730+ if(r.ok){const j=await r.json();FAV.ids=new Set(j.ids||[]);FAV.loaded=true;}
731+ }catch(e){}
732+ favPaint();
733+}
734+window.favToggle=async(id,el)=>{
735+ if(state.me===null){location.href="/api/auth/ka/login";return}
736+ const on=!FAV.ids.has(id);
737+ if(el)el.disabled=true;
738+ try{
739+ const r=await fetch("/api/favorites/toggle",{method:"POST",
740+ headers:{"Content-Type":"application/json"},
741+ body:JSON.stringify({on,item:FAV.src[id]||{item_id:id}})});
742+ if(r.status===401){location.href="/api/auth/ka/login";return}
743+ if(!r.ok)throw new Error(r.status);
744+ if(on)FAV.ids.add(id);else FAV.ids.delete(id);
745+ if(!on&&location.pathname==="/favoris"&&el){
746+ const card=el.closest(".card");if(card)card.remove();
747+ const grid=$("#fav-grid");
748+ if(grid&&!grid.querySelector(".card"))renderFavoris();
749+ }
750+ }catch(e){alert("Impossible de synchroniser le favori avec le hub Groupe KA — réessayez.")}
751+ if(el)el.disabled=false;
752+ favPaint();
753+};
754+
670 /* cascade de photos : avatar capté à la source, puis unavatar par plateforme, 755 /* cascade de photos : avatar capté à la source, puis unavatar par plateforme,
671 repli final = initiales sur motif encre/violet (déjà en dessous) */ 756 repli final = initiales sur motif encre/violet (déjà en dessous) */
672 function avatarSources(c){ 757 function avatarSources(c){
@@ -700,6 +785,7 @@ function header(){
700 <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span> 785 <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span>
701 <span class="header-spacer"></span> 786 <span class="header-spacer"></span>
702 <a class="hbtn" href="/stats" data-nav>Statistiques</a> 787 <a class="hbtn" href="/stats" data-nav>Statistiques</a>
788+ <a class="hbtn" href="/favoris" data-nav>♥ Favoris</a>
703 <a class="hbtn" href="/contact" data-nav>Contact</a> 789 <a class="hbtn" href="/contact" data-nav>Contact</a>
704 <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a> 790 <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a>
705 <span id="acct-slot"></span> 791 <span id="acct-slot"></span>
@@ -721,12 +807,29 @@ async function loadAccount(){
721 title="Se connecter avec KA ID"><span class="acct-long">Se connecter avec</span><span class="acct-short">Connexion</span> <span class="ka-badge">KA</span></a>`; 807 title="Se connecter avec KA ID"><span class="acct-long">Se connecter avec</span><span class="acct-short">Connexion</span> <span class="ka-badge">KA</span></a>`;
722 } 808 }
723 bindNav(slot); 809 bindNav(slot);
810+ if(me){favSlotPaint();favLoad();}
811+ else{FAV.ids=new Set();FAV.loaded=false;favSlotPaint();favPaint();}
812+}
813+/* CTA favori de la fiche créateur : ♥ si connecté, sinon « Se connecter avec KA ID » */
814+function favSlotPaint(){
815+ const s=document.getElementById("fav-slot");if(!s)return;
816+ const c=state.creator;if(!c)return;
817+ if(state.me){
818+ FAV.src[c.id]=favItem(c);
819+ const on=FAV.ids.has(c.id);
820+ s.innerHTML=`<button type="button" class="fav-cta${on?" on":""}" data-fav="${esc(c.id)}"
821+ aria-pressed="${on}" onclick="favToggle(this.dataset.fav,this)">${heartSvg}<span class="fav-cta-txt">${on?"Retirer de mes favoris":"Ajouter à mes favoris"}</span></button>`;
822+ }else if(state.me===null){
823+ s.innerHTML=`<a class="fav-cta" href="/api/auth/ka/login"
824+ title="Un seul compte pour tout le Groupe KA — favoris partagés « Mon univers Ka »">${heartSvg}<span>Se connecter avec KA ID</span></a>`;
825+ }
724 } 826 }
725 function tabbar(active){ 827 function tabbar(active){
726 const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`; 828 const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`;
727 return `<nav class="tabbar"> 829 return `<nav class="tabbar">
728 ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")} 830 ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")}
729 ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")} 831 ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")}
832+ ${t("/favoris","Favoris",`<svg viewBox="0 0 24 24"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`,"fav")}
730 ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")} 833 ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")}
731 ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")} 834 ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")}
732 </nav>`; 835 </nav>`;
@@ -740,6 +843,7 @@ function footer(){
740 return `<div class="footer-local"><div class="container"><nav> 843 return `<div class="footer-local"><div class="container"><nav>
741 <span class="klabel">Créa·Ka</span> 844 <span class="klabel">Créa·Ka</span>
742 <a href="/compte" data-nav>Mon compte KA ID</a> 845 <a href="/compte" data-nav>Mon compte KA ID</a>
846+ <a href="/favoris" data-nav>Mes favoris</a>
743 <a href="/stats" data-nav>Statistiques</a> 847 <a href="/stats" data-nav>Statistiques</a>
744 <a href="/retrait" data-nav>Demander un retrait</a> 848 <a href="/retrait" data-nav>Demander un retrait</a>
745 <a href="/contact" data-nav>Contact</a> 849 <a href="/contact" data-nav>Contact</a>
@@ -783,7 +887,7 @@ function cardHtml(c){
783 return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer"> 887 return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer">
784 <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)} 888 <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)}
785 <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> 889 <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
786− <span class="nplat">${np} plat.</span></div> 890+ <span class="nplat">${np} plat.</span>${favBtn(c)}</div>
787 <div class="card-body"> 891 <div class="card-body">
788 <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div> 892 <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div>
789 <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div> 893 <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div>
@@ -1023,6 +1127,7 @@ async function renderCreator(id){
1023 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))} 1127 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))}
1024 catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2> 1128 catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2>
1025 <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return} 1129 <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return}
1130+ state.creator=c;
1026 document.title=`${c.display_name} — Créa-Ka`; 1131 document.title=`${c.display_name} — Créa-Ka`;
1027 const verified=(c.platforms||[]).some(a=>a.verified); 1132 const verified=(c.platforms||[]).some(a=>a.verified);
1028 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join(""); 1133 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join("");
@@ -1089,7 +1194,8 @@ async function renderCreator(id){
1089 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> 1194 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
1090 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""} 1195 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""}
1091 ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div> 1196 ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div>
1092− ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}</div> 1197+ ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}
1198+ <div id="fav-slot" style="margin-top:16px"></div></div>
1093 </div> 1199 </div>
1094 <div class="hkpi-row">${heroKpis}</div> 1200 <div class="hkpi-row">${heroKpis}</div>
1095 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""} 1201 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""}
@@ -1630,6 +1736,65 @@ async function renderCompte(){
1630 state.me=null;history.pushState(null,"","/");route();}); 1736 state.me=null;history.pushState(null,"","/");route();});
1631 } 1737 }
1632 1738
1739+/* ---------- page favoris — « Mon univers Ka » (hub groupe-ka.com) ---------- */
1740+function favCardHtml(it){
1741+ const href=(it.url||"").replace(/^https?:\/\/(www\.)?crea-ka\.com/i,"")
1742+ ||("/createur/"+encodeURIComponent(it.item_id));
1743+ FAV.src[it.item_id]={item_id:it.item_id,title:it.title||"",subtitle:it.subtitle||"",
1744+ image_url:it.image_url||"",url:it.url||""};
1745+ return `<div class="card" data-href="${esc(href)}" role="link" tabindex="0" style="cursor:pointer">
1746+ <div class="card-photo"><span class="init">${esc(initials(it.title||"?"))}</span>
1747+ ${it.image_url?`<img loading="lazy" alt="${esc(it.title)}" src="${esc(it.image_url)}" onerror="this.remove()">`:""}
1748+ <button type="button" class="favbtn on" data-fav="${esc(it.item_id)}" aria-pressed="true"
1749+ aria-label="Retirer de mes favoris" title="Retirer de mes favoris"
1750+ onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button></div>
1751+ <div class="card-body">
1752+ <div class="card-name">${esc(it.title||it.item_id)}</div>
1753+ ${it.subtitle?`<div class="card-sub">${esc(it.subtitle)}</div>`:""}
1754+ </div></div>`;
1755+}
1756+async function renderFavoris(){
1757+ document.title="Mes favoris — Créa-Ka";
1758+ app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("fav");bindNav();
1759+ if(state.me===undefined){
1760+ try{const r=await fetch("/api/me");state.me=r.ok?await r.json():null}
1761+ catch(e){state.me=null}}
1762+ const shell=inner=>{app.innerHTML=header()+`
1763+ <div class="container favoris-page">
1764+ <span class="kicker">Mon univers Ka — favoris partagés</span>
1765+ <h1>Mes <span class="hl">favoris.</span></h1>
1766+ <p class="lead">Vos créateurs favoris, gardés dans votre compte KA ID sur groupe-ka.com :
1767+ les mêmes favoris vous suivent sur toutes les plateformes du Groupe KA.</p>
1768+ ${inner}
1769+ </div>`+footer()+tabbar("fav");bindNav();};
1770+ if(!state.me){
1771+ shell(`<div class="fav-empty" style="text-align:left;padding:34px 0">
1772+ <div class="compte-actions" style="margin-top:0">
1773+ <a class="btn btn-primary" href="/api/auth/ka/login">Se connecter avec KA ID</a>
1774+ <a class="btn btn-ghost" href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener">Créer un compte KA ID ↗</a>
1775+ </div>
1776+ <p style="font-size:12.5px;color:var(--ink-3);margin-top:16px">Un seul compte pour tout le
1777+ groupe — vos favoris sont entreposés au hub groupe-ka.com, jamais ici.</p></div>`);
1778+ loadAccount();return;
1779+ }
1780+ let data=null;
1781+ try{const r=await fetch("/api/favorites");if(r.ok)data=await r.json();}catch(e){}
1782+ if(!data){
1783+ shell(`<div class="fav-empty"><h2>Hub injoignable</h2>
1784+ <p>Impossible de lire vos favoris au hub Groupe KA pour le moment — réessayez dans un instant.</p></div>`);
1785+ loadAccount();return;
1786+ }
1787+ FAV.ids=new Set(data.ids||[]);FAV.loaded=true;
1788+ const items=(data.items||[]).filter(i=>i.item_id);
1789+ shell(items.length?`
1790+ <div class="count-line" style="padding-top:26px"><b>${fmtFull.format(items.length)}</b> créateur${items.length>1?"s":""} en favori</div>
1791+ <div class="grid" id="fav-grid">${items.map(favCardHtml).join("")}</div>`
1792+ :`<div class="fav-empty"><h2>Aucun favori pour l'instant</h2>
1793+ <p>Touchez le ♥ sur une carte ou une fiche de créateur pour l'ajouter à votre univers Ka.</p>
1794+ <p style="margin-top:16px"><a class="btn btn-primary" href="/#annuaire" data-nav>Explorer l'annuaire</a></p></div>`);
1795+ loadAccount();window.scrollTo(0,0);
1796+}
1797+
1633 /* ---------- page contact (écosystème Groupe KA — ka/ecosystem.json) ---------- */ 1798 /* ---------- page contact (écosystème Groupe KA — ka/ecosystem.json) ---------- */
1634 function renderContact(){ 1799 function renderContact(){
1635 document.title="Contact — Créa-Ka, un service Groupe KA"; 1800 document.title="Contact — Créa-Ka, un service Groupe KA";
@@ -1714,6 +1879,7 @@ function route(){
1714 if(m)r=renderCreator(decodeURIComponent(m[1])); 1879 if(m)r=renderCreator(decodeURIComponent(m[1]));
1715 else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();} 1880 else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();}
1716 else if(p==="/retrait")r=renderOptout(); 1881 else if(p==="/retrait")r=renderOptout();
1882+ else if(p==="/favoris")r=renderFavoris();
1717 else if(p==="/stats")r=renderStats(); 1883 else if(p==="/stats")r=renderStats();
1718 else if(p==="/compte")r=renderCompte(); 1884 else if(p==="/compte")r=renderCompte();
1719 else if(p==="/contact")r=renderContact(); 1885 else if(p==="/contact")r=renderContact();
1720 1886
Menu

Theme