Files
CoqJeux/index.html
T
alois cfb87387fe
Deploy CoqGames / deploy (push) Successful in 5s
ajout favicon
2026-05-23 08:48:22 +00:00

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>