SPB Git forge

spb/crea-ka

Public

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

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%

frontend: page créateur repensée — métriques calculées + onglet par plateforme + grille contenu images/vidéos

- bandeau KPI multi-plateforme calculé (portée, engagement moyen, cadence, contenus)
- onglets: Vue densemble (barres de portée) + un onglet par plateforme
- par plateforme: grille KPI détaillée + badges statut + grille de contenu récent (images/vidéos, play/durée, métriques) avec repli icône si CDN expiré

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 93a806e

2 changed files +472 −224

modified frontend/dist/index.html +236 −112
@@ -722,61 +722,91 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
722 722 background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);
723 723 padding:13px 17px;box-shadow:var(--shadow-off-soft)}
724 724 .sect-title{font-size:19px;margin:36px 0 15px;display:flex;align-items:center;gap:10px;text-transform:uppercase}
725 −.acc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}
726 −@media(max-width:640px){.acc-grid{grid-template-columns:1fr}}
727 −.acc-card{display:flex;flex-direction:column;background:var(--surface);
728 − border:1.5px solid var(--ink);border-radius:var(--r-card);
729 − box-shadow:var(--shadow-off-soft);transition:transform .16s ease,box-shadow .16s ease;
730 − min-width:0;overflow:hidden}
731 −.acc-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}
732 −.acc{display:flex;align-items:center;gap:13px;padding:14px 16px;min-width:0}
733 −/* rangée de stats riches */
734 −.astat-row{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 10px}
735 −.astat{display:flex;flex-direction:column;background:var(--surface-2);
736 − border:1px solid var(--line);border-radius:var(--r-ctl);padding:4px 9px;line-height:1.1}
737 −.astat b{font-family:var(--font-display);font-size:13px;color:var(--ink);font-weight:700}
738 −.astat span{font-family:var(--font-mono);font-size:8.5px;color:var(--ink-3);
739 − text-transform:uppercase;letter-spacing:.05em;font-weight:700;margin-top:1px}
740 −.abadge-row{display:flex;flex-wrap:wrap;gap:5px;padding:0 16px 12px}
741 −.abadge{display:inline-flex;background:var(--lime-soft);color:var(--green-deep);
742 − border:1px solid var(--line);border-radius:var(--r-ctl);padding:2px 8px;
743 − font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.02em}
744 −.abadge.live{background:#ffe3e0;color:#b0201a;border-color:#b0201a}
745 −/* bande de contenu récent */
746 −.cstrip{border-top:1px dashed var(--line);padding:11px 0 13px}
747 −.cstrip-h{font-family:var(--font-mono);font-size:9.5px;font-weight:700;color:var(--ink-3);
748 − text-transform:uppercase;letter-spacing:.07em;padding:0 16px 8px}
749 −.cstrip-scroll{display:flex;gap:9px;overflow-x:auto;padding:0 16px 3px;scroll-snap-type:x proximity}
750 −.cstrip-scroll::-webkit-scrollbar{height:5px}
751 −.cstrip-scroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
752 −.cstrip-item{flex:none;width:120px;scroll-snap-align:start;text-decoration:none;color:inherit;
753 − display:flex;flex-direction:column;gap:4px}
754 −.cstrip-thumb{position:relative;display:block;width:120px;height:120px;border-radius:9px;
755 − background-size:cover;background-position:center;background-color:var(--surface-2);
756 − border:1px solid var(--line);overflow:hidden}
757 −.cstrip-thumb.noimg{display:flex;align-items:center;justify-content:center}
758 −.cstrip-thumb.noimg svg{width:34px;height:34px;opacity:.35}
759 −.cstrip-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
760 − color:#fff;font-size:20px;text-shadow:0 1px 4px rgba(0,0,0,.6)}
761 −.cstrip-dur{position:absolute;right:5px;bottom:5px;background:rgba(0,0,0,.78);color:#fff;
762 − font-family:var(--font-mono);font-size:9px;padding:1px 5px;border-radius:5px}
763 −.cstrip-cap{font-size:11px;line-height:1.25;color:var(--ink-2);display:-webkit-box;
764 − -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
765 −.cstrip-meta{font-family:var(--font-mono);font-size:9px;color:var(--ink-3);line-height:1.2}
766 −.acc .plat{width:44px;height:44px;border-radius:9px;flex:none}
767 −.acc .plat svg{width:20px;height:20px}
768 −.acc-mid{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;overflow:hidden}
769 −.acc-name{font-family:var(--font-display);font-weight:700;font-size:14px;display:flex;align-items:center;gap:6px}
770 −.acc-handle{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;
771 − white-space:nowrap;display:block;max-width:100%}
772 −.acc-right{margin-left:auto;text-align:right;flex:none;padding-left:8px}
773 −.acc-right b{display:block;font-family:var(--font-display);font-size:16.5px;color:var(--ink);font-weight:700}
774 −.acc-right span{font-family:var(--font-mono);font-size:9px;color:var(--ink-3);
775 − text-transform:uppercase;letter-spacing:.06em;font-weight:700}
776 −.conf{display:inline-flex;padding:1px 7px;border-radius:var(--r-ctl);
777 − font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
725 +/* ===== page créateur : métriques calculées + onglets par plateforme ===== */
726 +.hkpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin:22px 0 6px}
727 +.hkpi{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);
728 + box-shadow:var(--shadow-off-soft);padding:13px 15px}
729 +.hkpi b{display:block;font-family:var(--font-display);font-size:22px;font-weight:800;color:var(--ink);line-height:1}
730 +.hkpi span{display:block;margin-top:5px;font-family:var(--font-mono);font-size:9.5px;color:var(--ink-3);
731 + text-transform:uppercase;letter-spacing:.05em;font-weight:700}
732 +/* barre d'onglets */
733 +.ptabs{display:flex;gap:8px;overflow-x:auto;margin:26px 0 0;padding-bottom:2px}
734 +.ptabs::-webkit-scrollbar{height:5px}.ptabs::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
735 +.ptab{flex:none;display:inline-flex;align-items:center;gap:7px;cursor:pointer;
736 + background:var(--surface);border:1.5px solid var(--ink);border-bottom:none;
737 + border-radius:12px 12px 0 0;padding:10px 15px;font-family:var(--font-display);
738 + font-weight:700;font-size:13px;color:var(--ink-2);transition:.14s;margin-bottom:-1.5px}
739 +.ptab:hover{background:var(--surface-2)}
740 +.ptab.on{background:var(--accent);color:var(--on-accent);border-color:var(--ink)}
741 +.ptab.on .ptab-ico{color:var(--on-accent)!important}
742 +.ptab-ico{width:16px;height:16px;display:inline-flex}.ptab-ico svg{width:16px;height:16px}
743 +.ptab-n{font-family:var(--font-mono);font-size:10px;opacity:.75;font-weight:700}
744 +.ppanels{border:1.5px solid var(--ink);border-radius:0 12px 12px 12px;
745 + background:var(--surface);box-shadow:var(--shadow-off-soft);padding:20px 18px 22px}
746 +.ppanel{display:none}.ppanel.on{display:block;animation:fade .2s ease}
747 +@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
748 +.ppanel-head{display:flex;align-items:center;gap:13px}
749 +.ppanel-head .plat{width:46px;height:46px;border-radius:10px;flex:none;display:flex;align-items:center;justify-content:center}
750 +.ppanel-head .plat svg{width:22px;height:22px}
751 +.ppanel-id{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
752 +.ppanel-name{font-family:var(--font-display);font-weight:800;font-size:17px;display:flex;align-items:center;gap:6px}
753 +.ppanel-handle{font-size:13px;color:var(--green);font-weight:600}
754 +.conf{display:inline-flex;padding:2px 9px;border-radius:var(--r-ctl);
755 + font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.03em;flex:none}
778 756 .conf.hi{background:var(--lime-soft);color:var(--green-deep)}
779 757 .conf.mid{background:var(--amber-soft);color:#8a5a12}
758 +.abadge-row{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0 4px}
759 +.abadge{display:inline-flex;background:var(--lime-soft);color:var(--green-deep);
760 + border:1px solid var(--line);border-radius:var(--r-ctl);padding:3px 9px;
761 + font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.02em}
762 +.abadge.ok{background:var(--surface-2);color:var(--ink-2)}
763 +.abadge.live{background:#ffe3e0;color:#b0201a;border-color:#b0201a}
764 +/* grille KPI par plateforme */
765 +.kpi-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin:16px 0 4px}
766 +.kpi{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl);padding:11px 13px}
767 +.kpi b{display:block;font-family:var(--font-display);font-size:19px;font-weight:800;color:var(--ink);line-height:1}
768 +.kpi span{display:block;margin-top:4px;font-family:var(--font-mono);font-size:9px;color:var(--ink-3);
769 + text-transform:uppercase;letter-spacing:.04em;font-weight:700}
770 +.ppanel-top{display:inline-flex;margin:12px 0 2px;background:var(--surface-2);border:1px solid var(--line);
771 + border-radius:var(--r-ctl);padding:8px 13px;font-size:12.5px;font-weight:700;color:var(--ink-2)}
772 +.ppanel-top:hover{color:var(--green)}
773 +.ppanel-foot{margin-top:14px;font-family:var(--font-mono);font-size:10px;color:var(--ink-3)}
774 +/* grille de contenu récent (images/vidéos) */
775 +.cg-title{font-family:var(--font-display);font-size:14px;font-weight:800;margin:20px 0 12px;color:var(--ink)}
776 +.cg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:13px}
777 +.cg-card{display:flex;flex-direction:column;text-decoration:none;color:inherit;
778 + border:1.5px solid var(--ink);border-radius:var(--r-card);overflow:hidden;
779 + background:var(--surface);box-shadow:var(--shadow-off-soft);transition:transform .15s,box-shadow .15s}
780 +.cg-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}
781 +.cg-thumb{position:relative;display:block;width:100%;aspect-ratio:1/1;background:var(--surface-2);overflow:hidden}
782 +.cg-thumb img{width:100%;height:100%;object-fit:cover;display:block}
783 +.cg-thumb.noimg{display:flex;align-items:center;justify-content:center}
784 +.cg-thumb.noimg svg{width:44px;height:44px;opacity:.3}
785 +.cg-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;
786 + font-size:26px;text-shadow:0 2px 6px rgba(0,0,0,.7);pointer-events:none}
787 +.cg-dur{position:absolute;right:6px;bottom:6px;background:rgba(0,0,0,.8);color:#fff;
788 + font-family:var(--font-mono);font-size:10px;padding:2px 6px;border-radius:5px}
789 +.cg-pin{position:absolute;left:6px;top:6px;font-size:13px}
790 +.cg-body{padding:9px 11px 11px;display:flex;flex-direction:column;gap:5px}
791 +.cg-cap{font-size:12px;line-height:1.3;color:var(--ink-2);display:-webkit-box;
792 + -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:31px}
793 +.cg-cap.dim{color:var(--ink-3)}
794 +.cg-meta{font-family:var(--font-mono);font-size:10px;color:var(--ink-3);display:flex;
795 + justify-content:space-between;gap:6px;flex-wrap:wrap}
796 +.cg-when{color:var(--ink-3);opacity:.8}
797 +.cg-empty{padding:26px;text-align:center;color:var(--ink-3);font-size:13px}
798 +.cg-empty svg{width:20px;height:20px;vertical-align:-4px;opacity:.4;margin-right:6px}
799 +/* vue d'ensemble : barres de portée */
800 +.obars{display:flex;flex-direction:column;gap:11px}
801 +.obar{display:flex;align-items:center;gap:11px}
802 +.obar-ico{width:32px;height:32px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center}
803 +.obar-ico svg{width:16px;height:16px}
804 +.obar-lab{width:96px;flex:none;font-family:var(--font-display);font-weight:700;font-size:13px}
805 +.obar-track{flex:1;height:12px;background:var(--surface-2);border:1px solid var(--line);border-radius:6px;overflow:hidden}
806 +.obar-fill{display:block;height:100%;border-radius:6px 0 0 6px;min-width:3px}
807 +.obar-val{width:78px;flex:none;text-align:right;font-family:var(--font-mono);font-size:12px;font-weight:700;color:var(--ink)}
808 +@media(max-width:640px){.obar-lab{width:64px;font-size:11px}.obar-val{width:60px;font-size:11px}
809 + .cg-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}}
780 810 .linkinbio{display:inline-flex;align-items:center;gap:10px;margin-top:22px;
781 811 background:var(--accent);color:var(--on-accent);border:1.5px solid var(--ink);
782 812 border-radius:var(--r-ctl);padding:11px 20px;font-family:var(--font-display);
@@ -1209,26 +1239,127 @@ async function reload(more){
1209 1239 const _relTime=ts=>{if(!ts)return"";if(typeof ts==="string"&&!/^\d+$/.test(ts))return esc(ts.slice(0,10));
1210 1240 const d=Math.floor((Date.now()/1000-Number(ts))/86400);
1211 1241 return d<=0?"aujourd'hui":d===1?"hier":d<30?d+" j":d<365?Math.floor(d/30)+" mois":Math.floor(d/365)+" ans";};
1212 −function contentStrip(a){
1213 − const m=a.metrics||{};
1214 − const items=m.recent_posts||m.recent_videos||m.recent_tweets||[];
1215 − if(!items.length)return"";
1216 − const cards=items.slice(0,8).map(it=>{
1242 +const _relTimeC=ts=>_relTime(ts);
1243 +/* image de contenu : IG/CDN expirent souvent → onerror bascule sur repli icône */
1244 +window.thumbFail=el=>{const w=el.parentElement;if(w){w.classList.add("noimg");
1245 + w.innerHTML=w.dataset.icon||"";}};
1246 +function _mediaItems(a){const m=a.metrics||{};
1247 + return m.recent_posts||m.recent_videos||m.recent_tweets||[];}
1248 +
1249 +/* grille de contenu récent, images/vidéos bien affichées */
1250 +function contentGrid(a){
1251 + const items=_mediaItems(a);
1252 + if(!items.length)return `<div class="cg-empty">${icon(a.platform)} Aucun contenu récent capté pour ce compte.</div>`;
1253 + const ico=icon(a.platform);
1254 + const cards=items.slice(0,12).map(it=>{
1217 1255 const img=it.display_url||it.thumbnail_url||"";
1218 − const title=esc((it.caption||it.title||it.text||"").slice(0,90));
1256 + const isVid=it.type==="video"||it.type==="reel"||!!it.duration||it.video_views!=null;
1257 + const cap=esc((it.caption||it.title||it.text||"").slice(0,140));
1258 + const when=_relTimeC(it.timestamp||it.published_at||it.published||it.created_at);
1219 1259 const stats=[
1220 − it.views!=null?fmt.format(it.views)+" vues":null,
1260 + it.views!=null?fmt.format(it.views)+" vues":(it.video_views!=null?fmt.format(it.video_views)+" vues":null),
1221 1261 it.likes!=null?fmt.format(it.likes)+" ❤":null,
1222 1262 it.comments!=null?fmt.format(it.comments)+" 💬":null,
1223 1263 it.retweets!=null?fmt.format(it.retweets)+" ↻":null,
1264 + it.shares!=null?fmt.format(it.shares)+" ↗":null,
1224 1265 ].filter(Boolean).join(" · ");
1225 − const when=_relTime(it.timestamp||it.published_at||it.published||it.created_at);
1226 − return `<a class="cstrip-item" href="${esc(it.url||a.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">
1227 − ${img?`<span class="cstrip-thumb" style="background-image:url('${esc(img)}')">${it.type==="video"||it.type==="reel"||it.duration?'<span class="cstrip-play">▶</span>':""}${it.duration?`<span class="cstrip-dur">${esc(it.duration)}</span>`:""}</span>`:`<span class="cstrip-thumb noimg">${icon(a.platform)}</span>`}
1228 − ${title?`<span class="cstrip-cap">${title}</span>`:""}
1229 − <span class="cstrip-meta">${stats}${when?` · ${when}`:""}</span></a>`}).join("");
1230 − return `<div class="cstrip"><div class="cstrip-h">Contenu récent</div><div class="cstrip-scroll">${cards}</div></div>`;
1266 + const thumb=img
1267 + ? `<span class="cg-thumb" data-icon='${esc(ico)}'><img loading="lazy" src="${esc(img)}" alt="" referrerpolicy="no-referrer" onerror="thumbFail(this)">
1268 + ${isVid?'<span class="cg-play">▶</span>':""}${it.duration?`<span class="cg-dur">${esc(it.duration)}</span>`:""}
1269 + ${it.is_pinned?'<span class="cg-pin">📌</span>':""}</span>`
1270 + : `<span class="cg-thumb noimg" data-icon='${esc(ico)}'>${ico}${isVid?'<span class="cg-play">▶</span>':""}</span>`;
1271 + return `<a class="cg-card" href="${esc(it.url||a.url)}" target="_blank" rel="noopener">
1272 + ${thumb}
1273 + <span class="cg-body">${cap?`<span class="cg-cap">${cap}</span>`:`<span class="cg-cap dim">Publication ${a.platform}</span>`}
1274 + <span class="cg-meta">${stats||"—"}${when?`<span class="cg-when">${when}</span>`:""}</span></span></a>`;
1275 + }).join("");
1276 + return `<div class="cg-grid">${cards}</div>`;
1277 +}
1278 +
1279 +/* définitions de métriques calculées, par plateforme */
1280 +function platformKPIs(a){
1281 + const m=a.metrics||{}, out=[];
1282 + const push=(v,l,suf)=>{if(v!=null&&v!=="")out.push([typeof v==="number"?fmt.format(v)+(suf||""):esc(v)+(suf||""),l]);};
1283 + push(a.followers,a.platform==="discord"?"membres":a.platform==="patreon"?"patrons":"abonnés");
1284 + push(m.engagement_rate_pct!=null?m.engagement_rate_pct:null,"engagement","%");
1285 + push(m.avg_likes,"j'aime / pub (moy.)");
1286 + push(m.avg_views!=null?m.avg_views:m.avg_video_views,"vues / pub (moy.)");
1287 + push(m.avg_comments,"comm. / pub (moy.)");
1288 + push(m.avg_retweets,"RT / pub (moy.)");
1289 + push(m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week,"pubs / semaine");
1290 + push(m.posts,"publications");
1291 + push(m.videos,"vidéos");
1292 + push(m.likes,a.platform==="tiktok"?"j'aime cumulés":"j'aime");
1293 + push(m.following,"abonnements");
1294 + push(m.online,"en ligne");
1295 + push(m.boosts,"boosts");
1296 + push(m.patrons,"patrons");
1297 + push(m.live_viewers,"spectateurs (live)");
1298 + return out;
1299 +}
1300 +function platformBadges(a){
1301 + const m=a.metrics||{};
1302 + return [
1303 + a.verified?'<span class="abadge ok">✓ Vérifié</span>':"",
1304 + m.is_partner?'<span class="abadge">Partenaire</span>':"",
1305 + m.is_affiliate&&!m.is_partner?'<span class="abadge">Affilié</span>':"",
1306 + m.partnered?'<span class="abadge">Serveur partenaire</span>':"",
1307 + m.is_live_now?'<span class="abadge live">● En direct</span>':"",
1308 + m.is_seller?'<span class="abadge">Boutique</span>':"",
1309 + m.is_free===true?'<span class="abadge">Gratuit</span>':"",
1310 + m.is_monthly?'<span class="abadge">Mensuel</span>':"",
1311 + m.has_story?'<span class="abadge">Story active</span>':"",
1312 + m.has_spotlight?'<span class="abadge">Spotlight</span>':"",
1313 + m.country?`<span class="abadge">${esc(m.country)}</span>`:"",
1314 + (m.recent_games||[]).slice(0,2).map(g=>`<span class="abadge">🎮 ${esc(g)}</span>`).join(""),
1315 + (m.top_hashtags||[]).slice(0,3).map(h=>`<span class="abadge">#${esc(h)}</span>`).join(""),
1316 + ].filter(Boolean).join("");
1317 +}
1318 +
1319 +/* panneau d'un compte (onglet plateforme) */
1320 +function platformPanel(a,idx,active){
1321 + const p=PLAT[a.platform]||PLAT.autre;
1322 + const conf=Math.round((a.confidence||0)*100);
1323 + const confCls=(a.confidence||0)>=0.75?"hi":"mid";
1324 + const kpis=platformKPIs(a).map(([v,l])=>`<div class="kpi"><b>${v}</b><span>${l}</span></div>`).join("");
1325 + const badges=platformBadges(a);
1326 + const stamp=(a.metrics||{})[a.platform+"_apify_at"]||a.last_checked;
1327 + const top=(a.metrics||{}).top_post||(a.metrics||{}).top_video||(a.metrics||{}).top_tweet;
1328 + return `<section class="ppanel${active?" on":""}" data-panel="${idx}">
1329 + <div class="ppanel-head">
1330 + <span class="plat" style="background:${p.color}">${icon(a.platform)}</span>
1331 + <div class="ppanel-id"><span class="ppanel-name">${p.label}${a.verified?checkSvg:""}</span>
1332 + <a class="ppanel-handle" href="${esc(a.url)}" target="_blank" rel="noopener">@${esc(a.handle)} ↗</a></div>
1333 + <span class="conf ${confCls}" title="Score de rattachement">${conf}%</span>
1334 + </div>
1335 + ${badges?`<div class="abadge-row">${badges}</div>`:""}
1336 + <div class="kpi-grid">${kpis||'<div class="kpi"><b>—</b><span>pas encore de métriques</span></div>'}</div>
1337 + ${top&&top.url?`<a class="ppanel-top" href="${esc(top.url)}" target="_blank" rel="noopener">★ Top contenu — ${top.views!=null?fmt.format(top.views)+" vues":""}${top.likes!=null?" · "+fmt.format(top.likes)+" ❤":""} ↗</a>`:""}
1338 + <h3 class="cg-title">Contenu récent</h3>
1339 + ${contentGrid(a)}
1340 + <div class="ppanel-foot">${stamp?"Dernière vérification "+esc(String(stamp).slice(0,10)):""}</div>
1341 + </section>`;
1342 +}
1343 +
1344 +window.showPtab=(i,el)=>{
1345 + document.querySelectorAll(".ppanel").forEach(s=>s.classList.toggle("on",s.dataset.panel==String(i)));
1346 + document.querySelectorAll(".ptab").forEach(t=>t.classList.remove("on"));
1347 + if(el)el.classList.add("on");
1348 + const ov=document.getElementById("ov-panel");if(ov)ov.classList.toggle("on",i==="ov");
1349 +};
1350 +
1351 +function computeAgg(c){
1352 + const P=(c.platforms||[]);
1353 + const reach=c.total_reach||P.reduce((s,a)=>s+(a.followers||0),0);
1354 + const eng=P.map(a=>(a.metrics||{}).engagement_rate_pct).filter(v=>v!=null);
1355 + const avgEng=eng.length?(eng.reduce((s,v)=>s+v,0)/eng.length):null;
1356 + const content=P.reduce((s,a)=>s+_mediaItems(a).length,0);
1357 + const cad=P.map(a=>{const m=a.metrics||{};return m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week;}).filter(v=>v!=null);
1358 + const main=P.slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];
1359 + return {reach,avgEng,content,maxCad:cad.length?Math.max(...cad):null,
1360 + nPlat:P.length,nVerif:P.filter(a=>a.verified).length,main};
1231 1361 }
1362 +
1232 1363 async function renderCreator(id){
1233 1364 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav();
1234 1365 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))}
@@ -1237,44 +1368,38 @@ async function renderCreator(id){
1237 1368 document.title=`${c.display_name} — Créa-Ka`;
1238 1369 const verified=(c.platforms||[]).some(a=>a.verified);
1239 1370 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join("");
1240 − const M_LBL={following:"abonnements",posts:"publications",likes:"j'aime",
1241 − views:"vues",videos:"vidéos",episodes:"épisodes",images:"photos",
1242 − friends:"amis",listed:"listes"};
1243 − const accs=(c.platforms||[]).map(a=>{const p=PLAT[a.platform]||PLAT.autre;
1244 − const conf=(a.confidence||0)>=0.75?["hi","confiance élevée"]:["mid","à confirmer"];
1245 − const m=a.metrics||{};
1246 − const mx=Object.entries(m).filter(([k,v])=>M_LBL[k]&&v!=null)
1247 − .map(([k,v])=>`${fmt.format(v)} ${M_LBL[k]}`).join(" · ");
1248 − // rangée de statistiques riches (enrichissement Apify multi-plateforme)
1249 − const stat=(v,l)=>v==null?"":`<span class="astat"><b>${typeof v==="number"?fmt.format(v):esc(v)}</b><span>${l}</span></span>`;
1250 − const stats=[
1251 − stat(m.engagement_rate_pct!=null?m.engagement_rate_pct+" %":null,"engagement"),
1252 − stat(m.avg_likes,"j'aime/moy"),
1253 − stat(m.avg_views!=null?m.avg_views:m.avg_video_views,"vues/moy"),
1254 − stat(m.avg_retweets,"RT/moy"),
1255 − stat(m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week,"/semaine"),
1256 − ].filter(Boolean).join("");
1257 − const badges=[
1258 − m.is_partner?'<span class="abadge">Partenaire</span>':"",
1259 − m.is_affiliate&&!m.is_partner?'<span class="abadge">Affilié</span>':"",
1260 − m.is_live_now?'<span class="abadge live">● En direct</span>':"",
1261 − m.is_seller?'<span class="abadge">Boutique</span>':"",
1262 − m.is_free===true?'<span class="abadge">Gratuit</span>':"",
1263 − m.has_story?'<span class="abadge">Story active</span>':"",
1264 − (m.recent_games||[]).length?`<span class="abadge">${esc((m.recent_games||[])[0])}</span>`:"",
1265 − (m.top_hashtags||[]).slice(0,2).map(h=>`<span class="abadge">#${esc(h)}</span>`).join(""),
1266 − ].filter(Boolean).join("");
1267 − return `<div class="acc-card">
1268 − <a class="acc" href="${esc(a.url)}" target="_blank" rel="noopener">
1269 − <span class="plat" style="background:${p.color}">${icon(a.platform)}</span>
1270 − <span class="acc-mid"><span class="acc-name">${p.label}${a.verified?checkSvg:""}</span>
1271 − <span class="acc-handle">@${esc(a.handle)}</span>
1272 − <span class="acc-handle" style="overflow:visible"><span class="conf ${conf[0]}" title="Score de rattachement ${Math.round((a.confidence||0)*100)} %">${conf[1]}</span>${mx?` <span style="font-size:11px">${mx}</span>`:""}</span></span>
1273 − <span class="acc-right">${a.followers?`<b>${fmt.format(a.followers)}</b><span>abonnés</span>`:`<span style="font-size:11px">voir le profil ↗</span>`}</span></a>
1274 − ${stats?`<div class="astat-row">${stats}</div>`:""}
1275 − ${badges?`<div class="abadge-row">${badges}</div>`:""}
1276 − ${contentStrip(a)}</div>`}).join("");
1277 − const checked=(c.platforms||[]).map(a=>(a.metrics||{}).instagram_apify_at||a.last_checked).filter(Boolean).sort().pop()||(c.platforms||[]).map(a=>a.last_checked).filter(Boolean).sort().pop();
1371 + const P=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0));
1372 + const g=computeAgg(c);
1373 + // bandeau de métriques CALCULÉES (multi-plateforme)
1374 + const kpi=(v,l)=>v==null?"":`<div class="hkpi"><b>${typeof v==="number"?fmt.format(v):esc(v)}</b><span>${l}</span></div>`;
1375 + const heroKpis=[
1376 + kpi(g.reach,"abonnés cumulés"),
1377 + kpi(g.nPlat,"plateformes"),
1378 + kpi(g.avgEng!=null?Math.round(g.avgEng*100)/100+" %":null,"engagement moyen"),
1379 + kpi(g.maxCad!=null?g.maxCad:null,"pubs / semaine"),
1380 + kpi(g.content||null,"contenus analysés"),
1381 + kpi(g.main?(PLAT[g.main.platform]||PLAT.autre).label:null,"plateforme nº 1"),
1382 + ].filter(Boolean).join("");
1383 + // barres comparatives de portée par plateforme (vue d'ensemble)
1384 + const maxF=Math.max(1,...P.map(a=>a.followers||0));
1385 + const bars=P.map(a=>{const p=PLAT[a.platform]||PLAT.autre;const w=Math.round((a.followers||0)/maxF*100);
1386 + return `<div class="obar"><span class="obar-ico" style="background:${p.color}">${icon(a.platform)}</span>
1387 + <span class="obar-lab">${p.label}</span>
1388 + <span class="obar-track"><span class="obar-fill" style="width:${w}%;background:${p.color}"></span></span>
1389 + <span class="obar-val">${a.followers!=null?fmt.format(a.followers):"—"}</span></div>`;}).join("");
1390 + // onglets + panneaux
1391 + const tabs=`<button class="ptab on" onclick="showPtab('ov',this)">Vue d'ensemble</button>`+
1392 + P.map((a,i)=>{const p=PLAT[a.platform]||PLAT.autre;
1393 + return `<button class="ptab" onclick="showPtab(${i},this)">
1394 + <span class="ptab-ico" style="color:${p.color}">${icon(a.platform)}</span>${p.label}
1395 + ${a.followers!=null?`<span class="ptab-n">${fmt.format(a.followers)}</span>`:""}</button>`;}).join("");
1396 + const ovPanel=`<section class="ppanel on" id="ov-panel" data-panel="ov">
1397 + <h3 class="cg-title">Portée par plateforme</h3>
1398 + <div class="obars">${bars||"<p class=klabel>Aucune plateforme.</p>"}</div>
1399 + ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""}
1400 + </section>`;
1401 + const panels=ovPanel+P.map((a,i)=>platformPanel(a,i,false)).join("");
1402 + const checked=(c.platforms||[]).map(a=>{const m=a.metrics||{};return m[a.platform+"_apify_at"]||a.last_checked;}).filter(Boolean).sort().pop();
1278 1403 app.innerHTML=header()+`
1279 1404 <div class="container cpage">
1280 1405 <a class="backlink" href="/" data-nav>← Tous les créateurs</a>
@@ -1283,16 +1408,15 @@ async function renderCreator(id){
1283 1408 <div style="flex:1;min-width:200px"><h1>${esc(c.display_name)}${verified?checkSvg:""}</h1>
1284 1409 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
1285 1410 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""}
1286 − ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div></div>
1287 − ${c.total_reach?`<div class="cpage-reach"><b>${fmt.format(c.total_reach)}</b><span>abonnés cumulés</span></div>`:""}
1411 + ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div>
1412 + ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}</div>
1288 1413 </div>
1289 − ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}
1414 + <div class="hkpi-row">${heroKpis}</div>
1290 1415 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""}
1291 − <h2 class="sect-title">Ses plateformes <span class="chip">${(c.platforms||[]).length}</span></h2>
1292 − <div class="acc-grid">${accs}</div>
1293 − ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""}
1294 − <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes.
1295 − Audiences approximatives${checked?`, dernière vérification ${esc(checked.slice(0,10))}`:""}.
1416 + <div class="ptabs">${tabs}</div>
1417 + <div class="ppanels">${panels}</div>
1418 + <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes (enrichissement Apify).
1419 + Audiences et métriques approximatives${checked?`, dernière vérification ${esc(String(checked).slice(0,10))}`:""}.
1296 1420 Une erreur dans cette fiche ? <a href="/retrait" data-nav>Signaler / demander un retrait</a>.</div>
1297 1421 </div>`+footer()+tabbar("");
1298 1422 bindNav();window.scrollTo(0,0);
modified frontend/src/index.template.html +236 −112
@@ -264,61 +264,91 @@ select.f.active{background-color:var(--accent-soft);color:var(--ink);border-colo
264 264 background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);
265 265 padding:13px 17px;box-shadow:var(--shadow-off-soft)}
266 266 .sect-title{font-size:19px;margin:36px 0 15px;display:flex;align-items:center;gap:10px;text-transform:uppercase}
267 −.acc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}
268 −@media(max-width:640px){.acc-grid{grid-template-columns:1fr}}
269 −.acc-card{display:flex;flex-direction:column;background:var(--surface);
270 − border:1.5px solid var(--ink);border-radius:var(--r-card);
271 − box-shadow:var(--shadow-off-soft);transition:transform .16s ease,box-shadow .16s ease;
272 − min-width:0;overflow:hidden}
273 −.acc-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}
274 −.acc{display:flex;align-items:center;gap:13px;padding:14px 16px;min-width:0}
275 −/* rangée de stats riches */
276 −.astat-row{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 10px}
277 −.astat{display:flex;flex-direction:column;background:var(--surface-2);
278 − border:1px solid var(--line);border-radius:var(--r-ctl);padding:4px 9px;line-height:1.1}
279 −.astat b{font-family:var(--font-display);font-size:13px;color:var(--ink);font-weight:700}
280 −.astat span{font-family:var(--font-mono);font-size:8.5px;color:var(--ink-3);
281 − text-transform:uppercase;letter-spacing:.05em;font-weight:700;margin-top:1px}
282 −.abadge-row{display:flex;flex-wrap:wrap;gap:5px;padding:0 16px 12px}
283 −.abadge{display:inline-flex;background:var(--lime-soft);color:var(--green-deep);
284 − border:1px solid var(--line);border-radius:var(--r-ctl);padding:2px 8px;
285 − font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.02em}
286 −.abadge.live{background:#ffe3e0;color:#b0201a;border-color:#b0201a}
287 −/* bande de contenu récent */
288 −.cstrip{border-top:1px dashed var(--line);padding:11px 0 13px}
289 −.cstrip-h{font-family:var(--font-mono);font-size:9.5px;font-weight:700;color:var(--ink-3);
290 − text-transform:uppercase;letter-spacing:.07em;padding:0 16px 8px}
291 −.cstrip-scroll{display:flex;gap:9px;overflow-x:auto;padding:0 16px 3px;scroll-snap-type:x proximity}
292 −.cstrip-scroll::-webkit-scrollbar{height:5px}
293 −.cstrip-scroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
294 −.cstrip-item{flex:none;width:120px;scroll-snap-align:start;text-decoration:none;color:inherit;
295 − display:flex;flex-direction:column;gap:4px}
296 −.cstrip-thumb{position:relative;display:block;width:120px;height:120px;border-radius:9px;
297 − background-size:cover;background-position:center;background-color:var(--surface-2);
298 − border:1px solid var(--line);overflow:hidden}
299 −.cstrip-thumb.noimg{display:flex;align-items:center;justify-content:center}
300 −.cstrip-thumb.noimg svg{width:34px;height:34px;opacity:.35}
301 −.cstrip-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
302 − color:#fff;font-size:20px;text-shadow:0 1px 4px rgba(0,0,0,.6)}
303 −.cstrip-dur{position:absolute;right:5px;bottom:5px;background:rgba(0,0,0,.78);color:#fff;
304 − font-family:var(--font-mono);font-size:9px;padding:1px 5px;border-radius:5px}
305 −.cstrip-cap{font-size:11px;line-height:1.25;color:var(--ink-2);display:-webkit-box;
306 − -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
307 −.cstrip-meta{font-family:var(--font-mono);font-size:9px;color:var(--ink-3);line-height:1.2}
308 −.acc .plat{width:44px;height:44px;border-radius:9px;flex:none}
309 −.acc .plat svg{width:20px;height:20px}
310 −.acc-mid{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;overflow:hidden}
311 −.acc-name{font-family:var(--font-display);font-weight:700;font-size:14px;display:flex;align-items:center;gap:6px}
312 −.acc-handle{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;
313 − white-space:nowrap;display:block;max-width:100%}
314 −.acc-right{margin-left:auto;text-align:right;flex:none;padding-left:8px}
315 −.acc-right b{display:block;font-family:var(--font-display);font-size:16.5px;color:var(--ink);font-weight:700}
316 −.acc-right span{font-family:var(--font-mono);font-size:9px;color:var(--ink-3);
317 − text-transform:uppercase;letter-spacing:.06em;font-weight:700}
318 −.conf{display:inline-flex;padding:1px 7px;border-radius:var(--r-ctl);
319 − font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
267 +/* ===== page créateur : métriques calculées + onglets par plateforme ===== */
268 +.hkpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin:22px 0 6px}
269 +.hkpi{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);
270 + box-shadow:var(--shadow-off-soft);padding:13px 15px}
271 +.hkpi b{display:block;font-family:var(--font-display);font-size:22px;font-weight:800;color:var(--ink);line-height:1}
272 +.hkpi span{display:block;margin-top:5px;font-family:var(--font-mono);font-size:9.5px;color:var(--ink-3);
273 + text-transform:uppercase;letter-spacing:.05em;font-weight:700}
274 +/* barre d'onglets */
275 +.ptabs{display:flex;gap:8px;overflow-x:auto;margin:26px 0 0;padding-bottom:2px}
276 +.ptabs::-webkit-scrollbar{height:5px}.ptabs::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
277 +.ptab{flex:none;display:inline-flex;align-items:center;gap:7px;cursor:pointer;
278 + background:var(--surface);border:1.5px solid var(--ink);border-bottom:none;
279 + border-radius:12px 12px 0 0;padding:10px 15px;font-family:var(--font-display);
280 + font-weight:700;font-size:13px;color:var(--ink-2);transition:.14s;margin-bottom:-1.5px}
281 +.ptab:hover{background:var(--surface-2)}
282 +.ptab.on{background:var(--accent);color:var(--on-accent);border-color:var(--ink)}
283 +.ptab.on .ptab-ico{color:var(--on-accent)!important}
284 +.ptab-ico{width:16px;height:16px;display:inline-flex}.ptab-ico svg{width:16px;height:16px}
285 +.ptab-n{font-family:var(--font-mono);font-size:10px;opacity:.75;font-weight:700}
286 +.ppanels{border:1.5px solid var(--ink);border-radius:0 12px 12px 12px;
287 + background:var(--surface);box-shadow:var(--shadow-off-soft);padding:20px 18px 22px}
288 +.ppanel{display:none}.ppanel.on{display:block;animation:fade .2s ease}
289 +@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
290 +.ppanel-head{display:flex;align-items:center;gap:13px}
291 +.ppanel-head .plat{width:46px;height:46px;border-radius:10px;flex:none;display:flex;align-items:center;justify-content:center}
292 +.ppanel-head .plat svg{width:22px;height:22px}
293 +.ppanel-id{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
294 +.ppanel-name{font-family:var(--font-display);font-weight:800;font-size:17px;display:flex;align-items:center;gap:6px}
295 +.ppanel-handle{font-size:13px;color:var(--green);font-weight:600}
296 +.conf{display:inline-flex;padding:2px 9px;border-radius:var(--r-ctl);
297 + font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.03em;flex:none}
320 298 .conf.hi{background:var(--lime-soft);color:var(--green-deep)}
321 299 .conf.mid{background:var(--amber-soft);color:#8a5a12}
300 +.abadge-row{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0 4px}
301 +.abadge{display:inline-flex;background:var(--lime-soft);color:var(--green-deep);
302 + border:1px solid var(--line);border-radius:var(--r-ctl);padding:3px 9px;
303 + font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.02em}
304 +.abadge.ok{background:var(--surface-2);color:var(--ink-2)}
305 +.abadge.live{background:#ffe3e0;color:#b0201a;border-color:#b0201a}
306 +/* grille KPI par plateforme */
307 +.kpi-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin:16px 0 4px}
308 +.kpi{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl);padding:11px 13px}
309 +.kpi b{display:block;font-family:var(--font-display);font-size:19px;font-weight:800;color:var(--ink);line-height:1}
310 +.kpi span{display:block;margin-top:4px;font-family:var(--font-mono);font-size:9px;color:var(--ink-3);
311 + text-transform:uppercase;letter-spacing:.04em;font-weight:700}
312 +.ppanel-top{display:inline-flex;margin:12px 0 2px;background:var(--surface-2);border:1px solid var(--line);
313 + border-radius:var(--r-ctl);padding:8px 13px;font-size:12.5px;font-weight:700;color:var(--ink-2)}
314 +.ppanel-top:hover{color:var(--green)}
315 +.ppanel-foot{margin-top:14px;font-family:var(--font-mono);font-size:10px;color:var(--ink-3)}
316 +/* grille de contenu récent (images/vidéos) */
317 +.cg-title{font-family:var(--font-display);font-size:14px;font-weight:800;margin:20px 0 12px;color:var(--ink)}
318 +.cg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:13px}
319 +.cg-card{display:flex;flex-direction:column;text-decoration:none;color:inherit;
320 + border:1.5px solid var(--ink);border-radius:var(--r-card);overflow:hidden;
321 + background:var(--surface);box-shadow:var(--shadow-off-soft);transition:transform .15s,box-shadow .15s}
322 +.cg-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}
323 +.cg-thumb{position:relative;display:block;width:100%;aspect-ratio:1/1;background:var(--surface-2);overflow:hidden}
324 +.cg-thumb img{width:100%;height:100%;object-fit:cover;display:block}
325 +.cg-thumb.noimg{display:flex;align-items:center;justify-content:center}
326 +.cg-thumb.noimg svg{width:44px;height:44px;opacity:.3}
327 +.cg-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;
328 + font-size:26px;text-shadow:0 2px 6px rgba(0,0,0,.7);pointer-events:none}
329 +.cg-dur{position:absolute;right:6px;bottom:6px;background:rgba(0,0,0,.8);color:#fff;
330 + font-family:var(--font-mono);font-size:10px;padding:2px 6px;border-radius:5px}
331 +.cg-pin{position:absolute;left:6px;top:6px;font-size:13px}
332 +.cg-body{padding:9px 11px 11px;display:flex;flex-direction:column;gap:5px}
333 +.cg-cap{font-size:12px;line-height:1.3;color:var(--ink-2);display:-webkit-box;
334 + -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:31px}
335 +.cg-cap.dim{color:var(--ink-3)}
336 +.cg-meta{font-family:var(--font-mono);font-size:10px;color:var(--ink-3);display:flex;
337 + justify-content:space-between;gap:6px;flex-wrap:wrap}
338 +.cg-when{color:var(--ink-3);opacity:.8}
339 +.cg-empty{padding:26px;text-align:center;color:var(--ink-3);font-size:13px}
340 +.cg-empty svg{width:20px;height:20px;vertical-align:-4px;opacity:.4;margin-right:6px}
341 +/* vue d'ensemble : barres de portée */
342 +.obars{display:flex;flex-direction:column;gap:11px}
343 +.obar{display:flex;align-items:center;gap:11px}
344 +.obar-ico{width:32px;height:32px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center}
345 +.obar-ico svg{width:16px;height:16px}
346 +.obar-lab{width:96px;flex:none;font-family:var(--font-display);font-weight:700;font-size:13px}
347 +.obar-track{flex:1;height:12px;background:var(--surface-2);border:1px solid var(--line);border-radius:6px;overflow:hidden}
348 +.obar-fill{display:block;height:100%;border-radius:6px 0 0 6px;min-width:3px}
349 +.obar-val{width:78px;flex:none;text-align:right;font-family:var(--font-mono);font-size:12px;font-weight:700;color:var(--ink)}
350 +@media(max-width:640px){.obar-lab{width:64px;font-size:11px}.obar-val{width:60px;font-size:11px}
351 + .cg-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}}
322 352 .linkinbio{display:inline-flex;align-items:center;gap:10px;margin-top:22px;
323 353 background:var(--accent);color:var(--on-accent);border:1.5px solid var(--ink);
324 354 border-radius:var(--r-ctl);padding:11px 20px;font-family:var(--font-display);
@@ -751,26 +781,127 @@ async function reload(more){
751 781 const _relTime=ts=>{if(!ts)return"";if(typeof ts==="string"&&!/^\d+$/.test(ts))return esc(ts.slice(0,10));
752 782 const d=Math.floor((Date.now()/1000-Number(ts))/86400);
753 783 return d<=0?"aujourd'hui":d===1?"hier":d<30?d+" j":d<365?Math.floor(d/30)+" mois":Math.floor(d/365)+" ans";};
754 −function contentStrip(a){
755 − const m=a.metrics||{};
756 − const items=m.recent_posts||m.recent_videos||m.recent_tweets||[];
757 − if(!items.length)return"";
758 − const cards=items.slice(0,8).map(it=>{
784 +const _relTimeC=ts=>_relTime(ts);
785 +/* image de contenu : IG/CDN expirent souvent → onerror bascule sur repli icône */
786 +window.thumbFail=el=>{const w=el.parentElement;if(w){w.classList.add("noimg");
787 + w.innerHTML=w.dataset.icon||"";}};
788 +function _mediaItems(a){const m=a.metrics||{};
789 + return m.recent_posts||m.recent_videos||m.recent_tweets||[];}
790 +
791 +/* grille de contenu récent, images/vidéos bien affichées */
792 +function contentGrid(a){
793 + const items=_mediaItems(a);
794 + if(!items.length)return `<div class="cg-empty">${icon(a.platform)} Aucun contenu récent capté pour ce compte.</div>`;
795 + const ico=icon(a.platform);
796 + const cards=items.slice(0,12).map(it=>{
759 797 const img=it.display_url||it.thumbnail_url||"";
760 − const title=esc((it.caption||it.title||it.text||"").slice(0,90));
798 + const isVid=it.type==="video"||it.type==="reel"||!!it.duration||it.video_views!=null;
799 + const cap=esc((it.caption||it.title||it.text||"").slice(0,140));
800 + const when=_relTimeC(it.timestamp||it.published_at||it.published||it.created_at);
761 801 const stats=[
762 − it.views!=null?fmt.format(it.views)+" vues":null,
802 + it.views!=null?fmt.format(it.views)+" vues":(it.video_views!=null?fmt.format(it.video_views)+" vues":null),
763 803 it.likes!=null?fmt.format(it.likes)+" ❤":null,
764 804 it.comments!=null?fmt.format(it.comments)+" 💬":null,
765 805 it.retweets!=null?fmt.format(it.retweets)+" ↻":null,
806 + it.shares!=null?fmt.format(it.shares)+" ↗":null,
766 807 ].filter(Boolean).join(" · ");
767 − const when=_relTime(it.timestamp||it.published_at||it.published||it.created_at);
768 − return `<a class="cstrip-item" href="${esc(it.url||a.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">
769 − ${img?`<span class="cstrip-thumb" style="background-image:url('${esc(img)}')">${it.type==="video"||it.type==="reel"||it.duration?'<span class="cstrip-play">▶</span>':""}${it.duration?`<span class="cstrip-dur">${esc(it.duration)}</span>`:""}</span>`:`<span class="cstrip-thumb noimg">${icon(a.platform)}</span>`}
770 − ${title?`<span class="cstrip-cap">${title}</span>`:""}
771 − <span class="cstrip-meta">${stats}${when?` · ${when}`:""}</span></a>`}).join("");
772 − return `<div class="cstrip"><div class="cstrip-h">Contenu récent</div><div class="cstrip-scroll">${cards}</div></div>`;
808 + const thumb=img
809 + ? `<span class="cg-thumb" data-icon='${esc(ico)}'><img loading="lazy" src="${esc(img)}" alt="" referrerpolicy="no-referrer" onerror="thumbFail(this)">
810 + ${isVid?'<span class="cg-play">▶</span>':""}${it.duration?`<span class="cg-dur">${esc(it.duration)}</span>`:""}
811 + ${it.is_pinned?'<span class="cg-pin">📌</span>':""}</span>`
812 + : `<span class="cg-thumb noimg" data-icon='${esc(ico)}'>${ico}${isVid?'<span class="cg-play">▶</span>':""}</span>`;
813 + return `<a class="cg-card" href="${esc(it.url||a.url)}" target="_blank" rel="noopener">
814 + ${thumb}
815 + <span class="cg-body">${cap?`<span class="cg-cap">${cap}</span>`:`<span class="cg-cap dim">Publication ${a.platform}</span>`}
816 + <span class="cg-meta">${stats||"—"}${when?`<span class="cg-when">${when}</span>`:""}</span></span></a>`;
817 + }).join("");
818 + return `<div class="cg-grid">${cards}</div>`;
819 +}
820 +
821 +/* définitions de métriques calculées, par plateforme */
822 +function platformKPIs(a){
823 + const m=a.metrics||{}, out=[];
824 + const push=(v,l,suf)=>{if(v!=null&&v!=="")out.push([typeof v==="number"?fmt.format(v)+(suf||""):esc(v)+(suf||""),l]);};
825 + push(a.followers,a.platform==="discord"?"membres":a.platform==="patreon"?"patrons":"abonnés");
826 + push(m.engagement_rate_pct!=null?m.engagement_rate_pct:null,"engagement","%");
827 + push(m.avg_likes,"j'aime / pub (moy.)");
828 + push(m.avg_views!=null?m.avg_views:m.avg_video_views,"vues / pub (moy.)");
829 + push(m.avg_comments,"comm. / pub (moy.)");
830 + push(m.avg_retweets,"RT / pub (moy.)");
831 + push(m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week,"pubs / semaine");
832 + push(m.posts,"publications");
833 + push(m.videos,"vidéos");
834 + push(m.likes,a.platform==="tiktok"?"j'aime cumulés":"j'aime");
835 + push(m.following,"abonnements");
836 + push(m.online,"en ligne");
837 + push(m.boosts,"boosts");
838 + push(m.patrons,"patrons");
839 + push(m.live_viewers,"spectateurs (live)");
840 + return out;
841 +}
842 +function platformBadges(a){
843 + const m=a.metrics||{};
844 + return [
845 + a.verified?'<span class="abadge ok">✓ Vérifié</span>':"",
846 + m.is_partner?'<span class="abadge">Partenaire</span>':"",
847 + m.is_affiliate&&!m.is_partner?'<span class="abadge">Affilié</span>':"",
848 + m.partnered?'<span class="abadge">Serveur partenaire</span>':"",
849 + m.is_live_now?'<span class="abadge live">● En direct</span>':"",
850 + m.is_seller?'<span class="abadge">Boutique</span>':"",
851 + m.is_free===true?'<span class="abadge">Gratuit</span>':"",
852 + m.is_monthly?'<span class="abadge">Mensuel</span>':"",
853 + m.has_story?'<span class="abadge">Story active</span>':"",
854 + m.has_spotlight?'<span class="abadge">Spotlight</span>':"",
855 + m.country?`<span class="abadge">${esc(m.country)}</span>`:"",
856 + (m.recent_games||[]).slice(0,2).map(g=>`<span class="abadge">🎮 ${esc(g)}</span>`).join(""),
857 + (m.top_hashtags||[]).slice(0,3).map(h=>`<span class="abadge">#${esc(h)}</span>`).join(""),
858 + ].filter(Boolean).join("");
773 859 }
860 +
861 +/* panneau d'un compte (onglet plateforme) */
862 +function platformPanel(a,idx,active){
863 + const p=PLAT[a.platform]||PLAT.autre;
864 + const conf=Math.round((a.confidence||0)*100);
865 + const confCls=(a.confidence||0)>=0.75?"hi":"mid";
866 + const kpis=platformKPIs(a).map(([v,l])=>`<div class="kpi"><b>${v}</b><span>${l}</span></div>`).join("");
867 + const badges=platformBadges(a);
868 + const stamp=(a.metrics||{})[a.platform+"_apify_at"]||a.last_checked;
869 + const top=(a.metrics||{}).top_post||(a.metrics||{}).top_video||(a.metrics||{}).top_tweet;
870 + return `<section class="ppanel${active?" on":""}" data-panel="${idx}">
871 + <div class="ppanel-head">
872 + <span class="plat" style="background:${p.color}">${icon(a.platform)}</span>
873 + <div class="ppanel-id"><span class="ppanel-name">${p.label}${a.verified?checkSvg:""}</span>
874 + <a class="ppanel-handle" href="${esc(a.url)}" target="_blank" rel="noopener">@${esc(a.handle)} ↗</a></div>
875 + <span class="conf ${confCls}" title="Score de rattachement">${conf}%</span>
876 + </div>
877 + ${badges?`<div class="abadge-row">${badges}</div>`:""}
878 + <div class="kpi-grid">${kpis||'<div class="kpi"><b>—</b><span>pas encore de métriques</span></div>'}</div>
879 + ${top&&top.url?`<a class="ppanel-top" href="${esc(top.url)}" target="_blank" rel="noopener">★ Top contenu — ${top.views!=null?fmt.format(top.views)+" vues":""}${top.likes!=null?" · "+fmt.format(top.likes)+" ❤":""} ↗</a>`:""}
880 + <h3 class="cg-title">Contenu récent</h3>
881 + ${contentGrid(a)}
882 + <div class="ppanel-foot">${stamp?"Dernière vérification "+esc(String(stamp).slice(0,10)):""}</div>
883 + </section>`;
884 +}
885 +
886 +window.showPtab=(i,el)=>{
887 + document.querySelectorAll(".ppanel").forEach(s=>s.classList.toggle("on",s.dataset.panel==String(i)));
888 + document.querySelectorAll(".ptab").forEach(t=>t.classList.remove("on"));
889 + if(el)el.classList.add("on");
890 + const ov=document.getElementById("ov-panel");if(ov)ov.classList.toggle("on",i==="ov");
891 +};
892 +
893 +function computeAgg(c){
894 + const P=(c.platforms||[]);
895 + const reach=c.total_reach||P.reduce((s,a)=>s+(a.followers||0),0);
896 + const eng=P.map(a=>(a.metrics||{}).engagement_rate_pct).filter(v=>v!=null);
897 + const avgEng=eng.length?(eng.reduce((s,v)=>s+v,0)/eng.length):null;
898 + const content=P.reduce((s,a)=>s+_mediaItems(a).length,0);
899 + const cad=P.map(a=>{const m=a.metrics||{};return m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week;}).filter(v=>v!=null);
900 + const main=P.slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];
901 + return {reach,avgEng,content,maxCad:cad.length?Math.max(...cad):null,
902 + nPlat:P.length,nVerif:P.filter(a=>a.verified).length,main};
903 +}
904 +
774 905 async function renderCreator(id){
775 906 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav();
776 907 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))}
@@ -779,44 +910,38 @@ async function renderCreator(id){
779 910 document.title=`${c.display_name} — Créa-Ka`;
780 911 const verified=(c.platforms||[]).some(a=>a.verified);
781 912 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join("");
782 − const M_LBL={following:"abonnements",posts:"publications",likes:"j'aime",
783 − views:"vues",videos:"vidéos",episodes:"épisodes",images:"photos",
784 − friends:"amis",listed:"listes"};
785 − const accs=(c.platforms||[]).map(a=>{const p=PLAT[a.platform]||PLAT.autre;
786 − const conf=(a.confidence||0)>=0.75?["hi","confiance élevée"]:["mid","à confirmer"];
787 − const m=a.metrics||{};
788 − const mx=Object.entries(m).filter(([k,v])=>M_LBL[k]&&v!=null)
789 − .map(([k,v])=>`${fmt.format(v)} ${M_LBL[k]}`).join(" · ");
790 − // rangée de statistiques riches (enrichissement Apify multi-plateforme)
791 − const stat=(v,l)=>v==null?"":`<span class="astat"><b>${typeof v==="number"?fmt.format(v):esc(v)}</b><span>${l}</span></span>`;
792 − const stats=[
793 − stat(m.engagement_rate_pct!=null?m.engagement_rate_pct+" %":null,"engagement"),
794 − stat(m.avg_likes,"j'aime/moy"),
795 − stat(m.avg_views!=null?m.avg_views:m.avg_video_views,"vues/moy"),
796 − stat(m.avg_retweets,"RT/moy"),
797 − stat(m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week,"/semaine"),
798 − ].filter(Boolean).join("");
799 − const badges=[
800 − m.is_partner?'<span class="abadge">Partenaire</span>':"",
801 − m.is_affiliate&&!m.is_partner?'<span class="abadge">Affilié</span>':"",
802 − m.is_live_now?'<span class="abadge live">● En direct</span>':"",
803 − m.is_seller?'<span class="abadge">Boutique</span>':"",
804 − m.is_free===true?'<span class="abadge">Gratuit</span>':"",
805 − m.has_story?'<span class="abadge">Story active</span>':"",
806 − (m.recent_games||[]).length?`<span class="abadge">${esc((m.recent_games||[])[0])}</span>`:"",
807 − (m.top_hashtags||[]).slice(0,2).map(h=>`<span class="abadge">#${esc(h)}</span>`).join(""),
808 − ].filter(Boolean).join("");
809 − return `<div class="acc-card">
810 − <a class="acc" href="${esc(a.url)}" target="_blank" rel="noopener">
811 − <span class="plat" style="background:${p.color}">${icon(a.platform)}</span>
812 − <span class="acc-mid"><span class="acc-name">${p.label}${a.verified?checkSvg:""}</span>
813 − <span class="acc-handle">@${esc(a.handle)}</span>
814 − <span class="acc-handle" style="overflow:visible"><span class="conf ${conf[0]}" title="Score de rattachement ${Math.round((a.confidence||0)*100)} %">${conf[1]}</span>${mx?` <span style="font-size:11px">${mx}</span>`:""}</span></span>
815 − <span class="acc-right">${a.followers?`<b>${fmt.format(a.followers)}</b><span>abonnés</span>`:`<span style="font-size:11px">voir le profil ↗</span>`}</span></a>
816 − ${stats?`<div class="astat-row">${stats}</div>`:""}
817 − ${badges?`<div class="abadge-row">${badges}</div>`:""}
818 − ${contentStrip(a)}</div>`}).join("");
819 − const checked=(c.platforms||[]).map(a=>(a.metrics||{}).instagram_apify_at||a.last_checked).filter(Boolean).sort().pop()||(c.platforms||[]).map(a=>a.last_checked).filter(Boolean).sort().pop();
913 + const P=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0));
914 + const g=computeAgg(c);
915 + // bandeau de métriques CALCULÉES (multi-plateforme)
916 + const kpi=(v,l)=>v==null?"":`<div class="hkpi"><b>${typeof v==="number"?fmt.format(v):esc(v)}</b><span>${l}</span></div>`;
917 + const heroKpis=[
918 + kpi(g.reach,"abonnés cumulés"),
919 + kpi(g.nPlat,"plateformes"),
920 + kpi(g.avgEng!=null?Math.round(g.avgEng*100)/100+" %":null,"engagement moyen"),
921 + kpi(g.maxCad!=null?g.maxCad:null,"pubs / semaine"),
922 + kpi(g.content||null,"contenus analysés"),
923 + kpi(g.main?(PLAT[g.main.platform]||PLAT.autre).label:null,"plateforme nº 1"),
924 + ].filter(Boolean).join("");
925 + // barres comparatives de portée par plateforme (vue d'ensemble)
926 + const maxF=Math.max(1,...P.map(a=>a.followers||0));
927 + const bars=P.map(a=>{const p=PLAT[a.platform]||PLAT.autre;const w=Math.round((a.followers||0)/maxF*100);
928 + return `<div class="obar"><span class="obar-ico" style="background:${p.color}">${icon(a.platform)}</span>
929 + <span class="obar-lab">${p.label}</span>
930 + <span class="obar-track"><span class="obar-fill" style="width:${w}%;background:${p.color}"></span></span>
931 + <span class="obar-val">${a.followers!=null?fmt.format(a.followers):"—"}</span></div>`;}).join("");
932 + // onglets + panneaux
933 + const tabs=`<button class="ptab on" onclick="showPtab('ov',this)">Vue d'ensemble</button>`+
934 + P.map((a,i)=>{const p=PLAT[a.platform]||PLAT.autre;
935 + return `<button class="ptab" onclick="showPtab(${i},this)">
936 + <span class="ptab-ico" style="color:${p.color}">${icon(a.platform)}</span>${p.label}
937 + ${a.followers!=null?`<span class="ptab-n">${fmt.format(a.followers)}</span>`:""}</button>`;}).join("");
938 + const ovPanel=`<section class="ppanel on" id="ov-panel" data-panel="ov">
939 + <h3 class="cg-title">Portée par plateforme</h3>
940 + <div class="obars">${bars||"<p class=klabel>Aucune plateforme.</p>"}</div>
941 + ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""}
942 + </section>`;
943 + const panels=ovPanel+P.map((a,i)=>platformPanel(a,i,false)).join("");
944 + const checked=(c.platforms||[]).map(a=>{const m=a.metrics||{};return m[a.platform+"_apify_at"]||a.last_checked;}).filter(Boolean).sort().pop();
820 945 app.innerHTML=header()+`
821 946 <div class="container cpage">
822 947 <a class="backlink" href="/" data-nav>← Tous les créateurs</a>
@@ -825,16 +950,15 @@ async function renderCreator(id){
825 950 <div style="flex:1;min-width:200px"><h1>${esc(c.display_name)}${verified?checkSvg:""}</h1>
826 951 <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>
827 952 ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""}
828 − ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div></div>
829 − ${c.total_reach?`<div class="cpage-reach"><b>${fmt.format(c.total_reach)}</b><span>abonnés cumulés</span></div>`:""}
953 + ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div>
954 + ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}</div>
830 955 </div>
831 − ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}
956 + <div class="hkpi-row">${heroKpis}</div>
832 957 ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""}
833 − <h2 class="sect-title">Ses plateformes <span class="chip">${(c.platforms||[]).length}</span></h2>
834 − <div class="acc-grid">${accs}</div>
835 − ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""}
836 − <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes.
837 − Audiences approximatives${checked?`, dernière vérification ${esc(checked.slice(0,10))}`:""}.
958 + <div class="ptabs">${tabs}</div>
959 + <div class="ppanels">${panels}</div>
960 + <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes (enrichissement Apify).
961 + Audiences et métriques approximatives${checked?`, dernière vérification ${esc(String(checked).slice(0,10))}`:""}.
838 962 Une erreur dans cette fiche ? <a href="/retrait" data-nav>Signaler / demander un retrait</a>.</div>
839 963 </div>`+footer()+tabbar("");
840 964 bindNav();window.scrollTo(0,0);
841 965