secubox-deb/packages/secubox-billets/www/billets/index.html
CyberMind-FR 5e77b9ebbd feat(billets): SecuBox webui panel + navbar integration
menu.d/415-billets.json (category mind) surfaces Billets in the SecuBox sidebar.
A funky, emoji-rich hybrid-dark panel at /billets/ (admin webui) with live stat
count-ups, reaction breakdown, latest billets and action buttons — fed by a new
CORS-enabled GET /stats.json (public aggregate counts). Fixed a latent import-time
BILLETS_DB read (now resolved at connect()). 89 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 16:19:35 +02:00

139 lines
7.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr" class="hybrid-dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>📮 Billets — SecuBox</title>
<link rel="stylesheet" href="/shared/hybrid-dark.css">
<style>
:root{ --b1:#6e40c9; --b2:#e63946; --b3:#00d4ff; --b4:#c9a84c; --ink:#e8e6d9; }
.billets-wrap{max-width:1000px;margin:0 auto;padding:1rem 1rem 4rem}
/* HERO */
.hero{position:relative;overflow:hidden;border-radius:22px;padding:2.4rem 2rem;
background:linear-gradient(120deg,#1b1030,#241436 40%,#101b28);
border:1px solid rgba(110,64,201,.4);box-shadow:0 20px 60px rgba(0,0,0,.45)}
.hero h1{margin:0;font-size:2.9rem;font-weight:800;letter-spacing:-.02em;
background:linear-gradient(90deg,#b98bff,#6e40c9 40%,#00d4ff);-webkit-background-clip:text;
background-clip:text;color:transparent}
.hero p{margin:.4rem 0 0;color:#b8b0d8;font-size:1.05rem}
.emoji-strip{margin-top:1rem;font-size:1.7rem;letter-spacing:.35rem}
.emoji-strip span{display:inline-block;animation:bob 3s ease-in-out infinite}
.emoji-strip span:nth-child(2n){animation-delay:.4s}
.emoji-strip span:nth-child(3n){animation-delay:.8s}
@keyframes bob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-9px) rotate(-6deg)}}
.hero .blob{position:absolute;border-radius:50%;filter:blur(42px);opacity:.5;z-index:0}
.hero .blob.a{width:220px;height:220px;background:#6e40c9;top:-70px;right:-40px}
.hero .blob.b{width:180px;height:180px;background:#00d4ff;bottom:-70px;left:20%}
.hero>*{position:relative;z-index:1}
/* STAT CARDS */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.9rem;margin:1.4rem 0}
.card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:16px;
padding:1.1rem;text-align:center;transition:transform .18s,border-color .18s}
.card:hover{transform:translateY(-4px);border-color:rgba(0,212,255,.5)}
.card .big{font-size:2.2rem;font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums}
.card .lab{color:#9a93b8;font-size:.82rem;margin-top:.2rem}
.card .em{font-size:1.5rem}
/* REACTION BAR */
.reacts{display:flex;flex-wrap:wrap;gap:.5rem;margin:.2rem 0 1.4rem}
.reacts .chip{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
border-radius:999px;padding:.35rem .8rem;font-size:1.05rem}
.reacts .chip b{color:var(--b3);font-variant-numeric:tabular-nums;margin-left:.2rem}
/* ACTIONS */
.actions{display:flex;flex-wrap:wrap;gap:.7rem;margin:1.2rem 0}
.btn{display:inline-flex;align-items:center;gap:.5rem;text-decoration:none;font-weight:700;
padding:.7rem 1.15rem;border-radius:12px;border:1px solid transparent;cursor:pointer;font-size:.98rem}
.btn.p{background:linear-gradient(120deg,#6e40c9,#8b5cf6);color:#fff}
.btn.s{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.14);color:var(--ink)}
.btn:hover{transform:translateY(-2px);box-shadow:0 10px 26px rgba(110,64,201,.35)}
/* LATEST */
.latest h2{font-size:1.15rem;color:var(--ink);margin:1.4rem 0 .6rem}
.latest a{display:flex;justify-content:space-between;gap:1rem;text-decoration:none;
padding:.7rem .9rem;border-radius:12px;border:1px solid rgba(255,255,255,.07);
background:rgba(255,255,255,.02);margin-bottom:.5rem;color:var(--ink);transition:border-color .15s}
.latest a:hover{border-color:rgba(0,212,255,.5)}
.latest .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.latest .d{color:#8a83a8;font-size:.8rem;flex:none}
.muted{color:#8a83a8}
</style>
</head>
<body class="hybrid-dark">
<nav class="sidebar" id="sidebar"></nav>
<script src="/shared/sidebar.js"></script>
<main class="main">
<div class="billets-wrap">
<section class="hero">
<span class="blob a"></span><span class="blob b"></span>
<h1>📮 Billets</h1>
<p>Micro-blog <b>gateway</b> inter-médias sociaux — publie court, embarque, republie 🌍</p>
<div class="emoji-strip" aria-hidden="true">
<span>👍</span><span>❤️</span><span>😂</span><span>😮</span><span>😢</span><span>🔥</span>
<span></span><span>📮</span><span>🚀</span>
</div>
</section>
<div class="cards" id="cards">
<div class="card"><div class="em">📝</div><div class="big" data-k="billets_published"></div><div class="lab">Billets publiés</div></div>
<div class="card"><div class="em">📄</div><div class="big" data-k="billets_drafts"></div><div class="lab">Brouillons</div></div>
<div class="card"><div class="em">💬</div><div class="big" data-k="comments_approved"></div><div class="lab">Commentaires</div></div>
<div class="card"><div class="em"></div><div class="big" data-k="reactions_total"></div><div class="lab">Réactions</div></div>
</div>
<div class="reacts" id="reacts"></div>
<div class="actions" id="actions">
<a class="btn p" id="a-open" target="_blank" rel="noopener">🚀 Ouvrir le blog</a>
<a class="btn s" id="a-new" target="_blank" rel="noopener">✍️ Nouveau billet</a>
<a class="btn s" id="a-admin" target="_blank" rel="noopener">🔐 Admin</a>
<a class="btn s" id="a-feed" target="_blank" rel="noopener">📡 Flux Atom</a>
</div>
<section class="latest">
<h2>🕒 Derniers billets</h2>
<div id="latest"><p class="muted">Chargement… ⏳</p></div>
</section>
</div>
</main>
<script>
(function(){
var FALLBACK = "https://billets.gk2.secubox.in";
var EMOJI_ORDER = ["👍","❤️","😂","😮","😢","🔥"];
function bump(el, to){ // count-up animation
var from=0, t0=performance.now(), dur=700;
function step(t){ var k=Math.min(1,(t-t0)/dur); el.textContent=Math.round(from+(to-from)*(k*(2-k))); if(k<1)requestAnimationFrame(step); }
requestAnimationFrame(step);
}
function links(site){
document.getElementById('a-open').href = site + "/";
document.getElementById('a-new').href = site + "/admin/billets/new";
document.getElementById('a-admin').href = site + "/admin";
document.getElementById('a-feed').href = site + "/feed.xml";
}
links(FALLBACK);
fetch(FALLBACK + "/stats.json", {mode:"cors"}).then(function(r){return r.json();}).then(function(s){
var site = (s.site_url||FALLBACK+"/").replace(/\/$/,"");
links(site);
document.querySelectorAll('.card .big[data-k]').forEach(function(el){
var v = s[el.getAttribute('data-k')]; if(typeof v==='number') bump(el, v); else el.textContent = v||0;
});
var rb = s.reactions_by||{}, rc=document.getElementById('reacts');
rc.innerHTML = EMOJI_ORDER.map(function(e){ return '<span class="chip">'+e+'<b>'+(rb[e]||0)+'</b></span>'; }).join('');
var lt = document.getElementById('latest');
if(s.latest && s.latest.length){
lt.innerHTML = s.latest.map(function(b){
var t=(b.title||'billet').replace(/[<>&]/g,function(c){return {'<':'&lt;','>':'&gt;','&':'&amp;'}[c];});
return '<a href="'+b.url+'" target="_blank" rel="noopener"><span class="t">'+t+'</span><span class="d">'+(b.date||'')+'</span></a>';
}).join('');
} else { lt.innerHTML = '<p class="muted">Aucun billet publié — clique ✍️ Nouveau billet.</p>'; }
}).catch(function(){
document.getElementById('latest').innerHTML = '<p class="muted">Blog injoignable pour l\'instant 😴 — <a href="'+FALLBACK+'" target="_blank" rel="noopener">ouvrir le blog</a>.</p>';
document.getElementById('reacts').innerHTML = EMOJI_ORDER.map(function(e){return '<span class="chip">'+e+'</span>';}).join('');
});
})();
</script>
</body>
</html>