396 lines
13 KiB
HTML
396 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<canvas id="bg-canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;pointer-events:none;"></canvas>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CoqJeux</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎮</text></svg>">
|
|
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
|
<style>
|
|
:root[data-theme="dark"] {
|
|
--bg: #0e0e12;
|
|
--bg-card-purple: #1a1025;
|
|
--border-purple: #3d2070;
|
|
--text-purple: #c4b5fd;
|
|
--avatar-purple: #2d1b4e;
|
|
--status-purple: #a78bfa;
|
|
--bg-card-teal: #0f1f1a;
|
|
--border-teal: #1a4a3a;
|
|
--text-teal: #6ee7b7;
|
|
--avatar-teal: #0d2e23;
|
|
--status-teal: #34d399;
|
|
--bg-card-amber: #1a1208;
|
|
--border-amber: #4a3010;
|
|
--text-amber: #fcd34d;
|
|
--avatar-amber: #2d1f08;
|
|
--status-amber: #fbbf24;
|
|
--bg-card-blue: #0f1525;
|
|
--border-blue: #1a2e5a;
|
|
--text-blue: #93c5fd;
|
|
--avatar-blue: #0d1e40;
|
|
--status-blue: #60a5fa;
|
|
--bg-card-coral: #1a0f0f;
|
|
--border-coral: #4a1a1a;
|
|
--text-coral: #fca5a5;
|
|
--avatar-coral: #2d0f0f;
|
|
--status-coral: #f87171;
|
|
--bg-card-green: #0f1a10;
|
|
--border-green: #1a4020;
|
|
--text-green: #86efac;
|
|
--avatar-green: #0d2810;
|
|
--status-green: #4ade80;
|
|
--bg-card-pink: #1a0f18;
|
|
--border-pink: #4a1a45;
|
|
--text-pink: #f0abfc;
|
|
--avatar-pink: #2d0f28;
|
|
--status-pink: #e879f9;
|
|
--header-title: #ffffff;
|
|
--header-sub: #666666;
|
|
--toggle-bg: #1e1e28;
|
|
--toggle-border: #333344;
|
|
--toggle-icon: #aaaaaa;
|
|
}
|
|
:root[data-theme="light"] {
|
|
--bg: #f5f5f7;
|
|
--bg-card-purple: #f3f0ff;
|
|
--border-purple: #c4b5fd;
|
|
--text-purple: #5b21b6;
|
|
--avatar-purple: #ede9fe;
|
|
--status-purple: #7c3aed;
|
|
--bg-card-teal: #f0fdf4;
|
|
--border-teal: #6ee7b7;
|
|
--text-teal: #065f46;
|
|
--avatar-teal: #d1fae5;
|
|
--status-teal: #059669;
|
|
--bg-card-amber: #fffbeb;
|
|
--border-amber: #fcd34d;
|
|
--text-amber: #92400e;
|
|
--avatar-amber: #fef3c7;
|
|
--status-amber: #d97706;
|
|
--bg-card-blue: #eff6ff;
|
|
--border-blue: #93c5fd;
|
|
--text-blue: #1e40af;
|
|
--avatar-blue: #dbeafe;
|
|
--status-blue: #2563eb;
|
|
--bg-card-coral: #fff1f2;
|
|
--border-coral: #fca5a5;
|
|
--text-coral: #9f1239;
|
|
--avatar-coral: #ffe4e6;
|
|
--status-coral: #e11d48;
|
|
--bg-card-green: #f0fdf4;
|
|
--border-green: #86efac;
|
|
--text-green: #14532d;
|
|
--avatar-green: #dcfce7;
|
|
--status-green: #16a34a;
|
|
--bg-card-pink: #fdf4ff;
|
|
--border-pink: #f0abfc;
|
|
--text-pink: #86198f;
|
|
--avatar-pink: #fae8ff;
|
|
--status-pink: #a21caf;
|
|
--header-title: #111111;
|
|
--header-sub: #888888;
|
|
--toggle-bg: #e5e5ea;
|
|
--toggle-border: #cccccc;
|
|
--toggle-icon: #555555;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html { font-size: 18px; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
min-height: 100vh;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
padding: 2rem 1.5rem;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.topbar { display: flex; justify-content: flex-end; margin-bottom: 1.5rem; }
|
|
|
|
.theme-toggle {
|
|
background: var(--toggle-bg);
|
|
border: 1px solid var(--toggle-border);
|
|
border-radius: 99px;
|
|
padding: 6px 14px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--toggle-icon);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: background 0.2s, border-color 0.2s;
|
|
}
|
|
.theme-toggle:hover { opacity: 0.8; }
|
|
|
|
header { text-align: center; margin-bottom: 2.5rem; }
|
|
header h1 { font-size: 2rem; font-weight: 700; color: var(--header-title); letter-spacing: -0.5px; }
|
|
header p { font-size: 14px; color: var(--header-sub); margin-top: 6px; }
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 14px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.card {
|
|
border-radius: 14px;
|
|
padding: 1.25rem 1rem;
|
|
border: 1px solid;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: transform 0.15s, opacity 0.15s;
|
|
}
|
|
.card:hover { transform: translateY(-3px); }
|
|
|
|
.card-top { display: flex; align-items: center; gap: 10px; }
|
|
|
|
.avatar {
|
|
width: 44px; height: 44px;
|
|
border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 22px; flex-shrink: 0;
|
|
}
|
|
|
|
.card-name { font-size: 15px; font-weight: 600; }
|
|
.card-desc { font-size: 12px; line-height: 1.55; opacity: 0.8; }
|
|
|
|
.status {
|
|
display: flex; align-items: center; gap: 6px;
|
|
font-size: 11px; margin-top: auto;
|
|
padding-top: 8px;
|
|
border-top: 1px solid rgba(128,128,128,0.15);
|
|
}
|
|
|
|
.dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
.dot-ok { background: #4ade80; }
|
|
.dot-warn { background: #facc15; animation: pulse 1.2s ease-in-out infinite; }
|
|
.dot-err { background: #f87171; animation: pulse 1.2s ease-in-out infinite; }
|
|
.dot-loading { background: #888; animation: pulse 1.2s ease-in-out infinite; }
|
|
|
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
|
|
.card.color-purple { background: var(--bg-card-purple); border-color: var(--border-purple); }
|
|
.card.color-purple .card-name { color: var(--text-purple); }
|
|
.card.color-purple .avatar { background: var(--avatar-purple); }
|
|
.card.color-purple .status { color: var(--status-purple); }
|
|
.card.color-purple .card-desc { color: var(--text-purple); }
|
|
|
|
.card.color-teal { background: var(--bg-card-teal); border-color: var(--border-teal); }
|
|
.card.color-teal .card-name { color: var(--text-teal); }
|
|
.card.color-teal .avatar { background: var(--avatar-teal); }
|
|
.card.color-teal .status { color: var(--status-teal); }
|
|
.card.color-teal .card-desc { color: var(--text-teal); }
|
|
|
|
.card.color-amber { background: var(--bg-card-amber); border-color: var(--border-amber); }
|
|
.card.color-amber .card-name { color: var(--text-amber); }
|
|
.card.color-amber .avatar { background: var(--avatar-amber); }
|
|
.card.color-amber .status { color: var(--status-amber); }
|
|
.card.color-amber .card-desc { color: var(--text-amber); }
|
|
|
|
.card.color-blue { background: var(--bg-card-blue); border-color: var(--border-blue); }
|
|
.card.color-blue .card-name { color: var(--text-blue); }
|
|
.card.color-blue .avatar { background: var(--avatar-blue); }
|
|
.card.color-blue .status { color: var(--status-blue); }
|
|
.card.color-blue .card-desc { color: var(--text-blue); }
|
|
|
|
.card.color-coral { background: var(--bg-card-coral); border-color: var(--border-coral); }
|
|
.card.color-coral .card-name { color: var(--text-coral); }
|
|
.card.color-coral .avatar { background: var(--avatar-coral); }
|
|
.card.color-coral .status { color: var(--status-coral); }
|
|
.card.color-coral .card-desc { color: var(--text-coral); }
|
|
|
|
.card.color-green { background: var(--bg-card-green); border-color: var(--border-green); }
|
|
.card.color-green .card-name { color: var(--text-green); }
|
|
.card.color-green .avatar { background: var(--avatar-green); }
|
|
.card.color-green .status { color: var(--status-green); }
|
|
.card.color-green .card-desc { color: var(--text-green); }
|
|
|
|
.card.color-pink { background: var(--bg-card-pink); border-color: var(--border-pink); }
|
|
.card.color-pink .card-name { color: var(--text-pink); }
|
|
.card.color-pink .avatar { background: var(--avatar-pink); }
|
|
.card.color-pink .status { color: var(--status-pink); }
|
|
.card.color-pink .card-desc { color: var(--text-pink); }
|
|
|
|
.error { text-align: center; color: #888; padding: 3rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="topbar">
|
|
<button class="theme-toggle" id="toggle" aria-label="Changer le thème">
|
|
<span id="toggle-icon">☀️</span>
|
|
<span id="toggle-label">Mode clair</span>
|
|
</button>
|
|
</div>
|
|
|
|
<header>
|
|
<h1>🎮 Coq Jeux, les jeux du Coquillard !</h1>
|
|
<p>Choisis ton jeu et rejoins la partie</p>
|
|
</header>
|
|
|
|
<div class="grid" id="grid">
|
|
<div class="error">Chargement des jeux...</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const canvas = document.getElementById('bg-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
const blobs = Array.from({length: 5}, (_, i) => ({
|
|
x: Math.random() * window.innerWidth,
|
|
y: Math.random() * window.innerHeight,
|
|
r: 250 + Math.random() * 200,
|
|
dx: (Math.random() - 0.5) * 0.4,
|
|
dy: (Math.random() - 0.5) * 0.4,
|
|
hue: i * 72,
|
|
}));
|
|
function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; }
|
|
function draw() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
blobs.forEach(b => {
|
|
b.x += b.dx; b.y += b.dy;
|
|
if (b.x < -b.r) b.x = canvas.width + b.r;
|
|
if (b.x > canvas.width + b.r) b.x = -b.r;
|
|
if (b.y < -b.r) b.y = canvas.height + b.r;
|
|
if (b.y > canvas.height + b.r) b.y = -b.r;
|
|
b.hue = (b.hue + 0.05) % 360;
|
|
const alpha = isDark() ? 0.18 : 0.14;
|
|
const grad = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
|
|
grad.addColorStop(0, `hsla(${b.hue}, 70%, ${isDark() ? 60 : 50}%, ${alpha})`);
|
|
grad.addColorStop(1, `hsla(${b.hue}, 70%, ${isDark() ? 60 : 50}%, 0)`);
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
requestAnimationFrame(draw);
|
|
}
|
|
draw();
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
const GATUS_URL = 'https://status.alocoq.fr/api/v1/endpoints/statuses';
|
|
const YAML_URL = './games.yaml';
|
|
|
|
const html = document.documentElement;
|
|
const toggle = document.getElementById('toggle');
|
|
const toggleIcon = document.getElementById('toggle-icon');
|
|
const toggleLabel = document.getElementById('toggle-label');
|
|
|
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
html.setAttribute('data-theme', savedTheme);
|
|
updateToggleLabel(savedTheme);
|
|
|
|
toggle.addEventListener('click', () => {
|
|
const current = html.getAttribute('data-theme');
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
html.setAttribute('data-theme', next);
|
|
localStorage.setItem('theme', next);
|
|
updateToggleLabel(next);
|
|
});
|
|
|
|
function updateToggleLabel(theme) {
|
|
if (theme === 'dark') {
|
|
toggleIcon.textContent = '☀️';
|
|
toggleLabel.textContent = 'Mode clair';
|
|
} else {
|
|
toggleIcon.textContent = '🌙';
|
|
toggleLabel.textContent = 'Mode sombre';
|
|
}
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
const s = Math.round(ms / 1000);
|
|
if (s < 60) return s + 's';
|
|
const m = Math.round(s / 60);
|
|
if (m < 60) return m + 'min';
|
|
const h = Math.floor(m / 60);
|
|
const rm = m % 60;
|
|
return rm > 0 ? h + 'h ' + rm + 'min' : h + 'h';
|
|
}
|
|
|
|
function getStatus(results) {
|
|
if (!results || results.length === 0) return { state: 'loading', text: 'Inconnu' };
|
|
const last = results[0];
|
|
|
|
if (last.success) {
|
|
const lastFailure = results.find(r => !r.success);
|
|
if (!lastFailure) return { state: 'ok', text: 'En ligne' };
|
|
const upSinceMs = new Date(last.timestamp) - new Date(lastFailure.timestamp);
|
|
if (upSinceMs > 24 * 60 * 60 * 1000) return { state: 'ok', text: 'En ligne' };
|
|
return { state: 'ok', text: 'En ligne depuis ' + formatDuration(upSinceMs) };
|
|
}
|
|
|
|
const downSinceMs = Date.now() - new Date(last.timestamp).getTime();
|
|
const lastSuccess = results.find(r => r.success);
|
|
if (lastSuccess) return { state: 'warn', text: 'Hors ligne depuis ' + formatDuration(downSinceMs) };
|
|
return { state: 'err', text: 'Hors ligne depuis ' + formatDuration(downSinceMs) };
|
|
}
|
|
|
|
function renderCard(game, statusData) {
|
|
const statuses = statusData[game.gatus_key] || null;
|
|
const { state, text } = getStatus(statuses);
|
|
const dotClass = { ok: 'dot-ok', warn: 'dot-warn', err: 'dot-err', loading: 'dot-loading' }[state];
|
|
const card = document.createElement('a');
|
|
card.href = game.url;
|
|
card.target = '_blank';
|
|
card.rel = 'noopener noreferrer';
|
|
card.className = 'card color-' + (game.color || 'purple');
|
|
card.innerHTML = `
|
|
<div class="card-top">
|
|
<div class="avatar">${game.avatar}</div>
|
|
<div class="card-name">${game.name}</div>
|
|
</div>
|
|
<div class="card-desc">${game.description}</div>
|
|
<div class="status">
|
|
<div class="dot ${dotClass}"></div>
|
|
<span>${text}</span>
|
|
</div>
|
|
`;
|
|
return card;
|
|
}
|
|
|
|
async function fetchGatusStatuses() {
|
|
try {
|
|
const res = await fetch(GATUS_URL);
|
|
if (!res.ok) return {};
|
|
const data = await res.json();
|
|
const map = {};
|
|
data.forEach(ep => { map[ep.key] = ep.results; });
|
|
return map;
|
|
} catch (e) {
|
|
console.error('Gatus fetch error:', e);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
const grid = document.getElementById('grid');
|
|
try {
|
|
const [yamlRes, statusData] = await Promise.all([
|
|
fetch(YAML_URL).then(r => r.text()),
|
|
fetchGatusStatuses(),
|
|
]);
|
|
const config = jsyaml.load(yamlRes);
|
|
grid.innerHTML = '';
|
|
config.games.forEach(game => { grid.appendChild(renderCard(game, statusData)); });
|
|
} catch (e) {
|
|
grid.innerHTML = '<div class="error">Impossible de charger les jeux.<br>' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |