metalfrom.eu/apps/web/quizz-site/index.html
Nicolas Fryder d4e1bf2e15 chore: initial commit — snapshot VPS 2026-06-27
- apps/api      : API REST Node.js/Fastify + PostgreSQL/PostGIS
- apps/geocoder : Worker Python géocodage Nominatim
- apps/worker   : Scraper Python Metal-Archives
- apps/web      : Frontend statique Leaflet/clustering/heatmap
- infra/        : docker-compose + init.sql

Déployé via Coolify + Traefik sur VPS OVH.
2026-06-27 08:53:39 +00:00

696 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>✨ Le Quiz des Équipes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,700;0,900;1,700&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0f0e17;
--surface: #1a1929;
--border: rgba(255,255,255,0.08);
--text: #fffffe;
--muted: rgba(255,255,255,0.45);
--saiga: #f7b731;
--axolotl: #26d0ce;
--ratel: #ff6b6b;
--poule: #ffd166;
--echidne: #c77dff;
--accent: #ff6b6b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Nunito', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 60% 50% at 20% 10%, rgba(199,125,255,0.12) 0%, transparent 60%),
radial-gradient(ellipse 50% 40% at 80% 90%, rgba(255,107,107,0.1) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
.app {
position: relative;
z-index: 1;
width: 100%;
max-width: 560px;
padding: 1.5rem 1.25rem 2rem;
}
/* ─── SCREEN TRANSITIONS ─── */
.screen {
display: none;
animation: fadeUp .45s cubic-bezier(.22,1,.36,1) both;
}
.screen.active { display: block; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── HEADER ─── */
.quiz-header {
text-align: center;
margin-bottom: 2rem;
}
.quiz-header .eyebrow {
font-size: .75rem;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: .4rem;
}
.quiz-header h1 {
font-family: 'Fraunces', serif;
font-size: clamp(2rem, 8vw, 2.8rem);
font-weight: 900;
line-height: 1.05;
background: linear-gradient(135deg, #fff 30%, var(--echidne));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.quiz-header p {
margin-top: .6rem;
color: var(--muted);
font-size: .9rem;
line-height: 1.5;
}
/* ─── NAME SELECTION ─── */
.search-wrap {
position: relative;
margin-bottom: 1.2rem;
}
.search-wrap input {
width: 100%;
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 14px;
padding: .85rem 1.1rem .85rem 2.8rem;
color: var(--text);
font-family: 'Nunito', sans-serif;
font-size: 1rem;
font-weight: 600;
outline: none;
transition: border-color .2s;
}
.search-wrap input:focus { border-color: rgba(199,125,255,.5); }
.search-wrap input::placeholder { color: var(--muted); font-weight: 400; }
.search-icon {
position: absolute;
left: .95rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
font-size: 1rem;
pointer-events: none;
}
.names-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: .55rem;
max-height: 58vh;
overflow-y: auto;
padding-right: .2rem;
}
.names-grid::-webkit-scrollbar { width: 4px; }
.names-grid::-webkit-scrollbar-track { background: transparent; }
.names-grid::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 4px; }
.name-btn {
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 12px;
padding: .7rem .9rem;
color: var(--text);
font-family: 'Nunito', sans-serif;
font-size: .88rem;
font-weight: 700;
cursor: pointer;
text-align: left;
transition: all .18s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name-btn:hover {
border-color: rgba(199,125,255,.5);
background: rgba(199,125,255,.08);
transform: translateY(-1px);
}
.name-btn:active { transform: scale(.96); }
.no-results {
text-align: center;
color: var(--muted);
padding: 2rem 0;
font-size: .9rem;
}
/* ─── QUIZ ─── */
.progress-track {
background: rgba(255,255,255,.08);
border-radius: 99px;
height: 4px;
margin-bottom: 2rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--echidne), var(--ratel));
border-radius: 99px;
transition: width .4s cubic-bezier(.22,1,.36,1);
}
.question-meta {
font-size: .75rem;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: .7rem;
}
.question-text {
font-family: 'Fraunces', serif;
font-size: clamp(1.25rem, 5vw, 1.6rem);
font-weight: 700;
line-height: 1.25;
margin-bottom: 1.6rem;
}
.choices {
display: flex;
flex-direction: column;
gap: .65rem;
}
.choice-btn {
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 14px;
padding: .95rem 1.15rem;
color: var(--text);
font-family: 'Nunito', sans-serif;
font-size: .9rem;
font-weight: 600;
cursor: pointer;
text-align: left;
display: flex;
align-items: flex-start;
gap: .75rem;
transition: all .18s;
line-height: 1.4;
}
.choice-btn .letter {
font-size: .75rem;
font-weight: 800;
letter-spacing: .06em;
color: var(--muted);
background: rgba(255,255,255,.07);
border-radius: 6px;
padding: .2rem .45rem;
flex-shrink: 0;
margin-top: .05rem;
}
.choice-btn:hover {
border-color: rgba(199,125,255,.5);
background: rgba(199,125,255,.07);
transform: translateX(4px);
}
.choice-btn:active { transform: scale(.97); }
.choice-btn.selected {
border-color: var(--echidne);
background: rgba(199,125,255,.13);
}
.choice-btn.selected .letter {
background: var(--echidne);
color: var(--bg);
}
.quiz-nav {
display: flex;
justify-content: flex-end;
margin-top: 1.4rem;
}
.btn-next {
background: var(--text);
color: var(--bg);
border: none;
border-radius: 12px;
padding: .8rem 1.8rem;
font-family: 'Nunito', sans-serif;
font-size: .9rem;
font-weight: 800;
cursor: pointer;
transition: all .18s;
opacity: 0;
pointer-events: none;
}
.btn-next.visible { opacity: 1; pointer-events: all; }
.btn-next:hover { transform: scale(1.04); }
.btn-next:active { transform: scale(.97); }
/* ─── RESULT ─── */
.result-screen {
text-align: center;
padding-top: .5rem;
}
.result-avatar-wrap {
width: 200px;
height: 200px;
border-radius: 50%;
margin: 0 auto 1.2rem;
padding: 3px;
animation: popIn .5s cubic-bezier(.34,1.56,.64,1) .1s both;
/* border color set inline via JS */
}
.result-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
display: block;
background: var(--surface);
}
@keyframes popIn {
from { opacity: 0; transform: scale(.4) rotate(-10deg); }
to { opacity: 1; transform: scale(1) rotate(0deg); }
}
.result-greeting {
font-size: .85rem;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: .3rem;
animation: fadeUp .4s .2s both;
}
.result-name {
font-family: 'Fraunces', serif;
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 1rem;
animation: fadeUp .4s .25s both;
}
.result-badge {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .5rem 1.4rem;
border-radius: 99px;
font-family: 'Fraunces', serif;
font-size: 1.8rem;
font-weight: 900;
letter-spacing: .03em;
margin-bottom: 1.4rem;
animation: fadeUp .4s .3s both;
}
.result-desc {
font-size: 1rem;
line-height: 1.65;
color: rgba(255,255,255,.8);
max-width: 360px;
margin: 0 auto 1.6rem;
animation: fadeUp .4s .35s both;
font-style: italic;
font-family: 'Fraunces', serif;
}
.btn-restart {
background: transparent;
border: 1.5px solid var(--border);
border-radius: 12px;
padding: .75rem 1.6rem;
color: var(--muted);
font-family: 'Nunito', sans-serif;
font-size: .85rem;
font-weight: 700;
cursor: pointer;
transition: all .18s;
animation: fadeUp .4s .5s both;
}
.btn-restart:hover { color: var(--text); border-color: rgba(255,255,255,.3); }
/* ─── CONFETTI ─── */
.confetti-container {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 10;
}
.confetto {
position: absolute;
width: 9px;
height: 9px;
border-radius: 2px;
top: -20px;
animation: confettiFall linear forwards;
}
@keyframes confettiFall {
0% { transform: translateY(0) rotate(0deg); opacity: 1; }
80% { opacity: 1; }
100% { transform: translateY(105vh) rotate(720deg); opacity: 0; }
}
</style>
</head>
<body>
<div class="confetti-container" id="confetti"></div>
<div class="app">
<!-- ═══ SCREEN 1: NAME SELECTION ═══ -->
<div class="screen active" id="screen-name">
<div class="quiz-header">
<div class="eyebrow">🎉 Weekend Anniversaire</div>
<h1>Qui es-tu<br>ce weekend ?</h1>
<p>Sélectionne ton prénom pour découvrir<br>dans quelle équipe tu es.</p>
</div>
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input type="text" id="name-search" placeholder="Recherche ton prénom…" autocomplete="off" />
</div>
<div class="names-grid" id="names-grid"></div>
</div>
<!-- ═══ SCREEN 2: QUIZ ═══ -->
<div class="screen" id="screen-quiz">
<div class="progress-track">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<div class="question-meta" id="q-meta"></div>
<div class="question-text" id="q-text"></div>
<div class="choices" id="q-choices"></div>
<div class="quiz-nav">
<button class="btn-next" id="btn-next">Suivant →</button>
</div>
</div>
<!-- ═══ SCREEN 3: RESULT ═══ -->
<div class="screen" id="screen-result">
<div class="result-screen">
<div class="result-avatar-wrap" id="r-avatar-wrap">
<img class="result-avatar" id="r-avatar" src="" alt="" />
</div>
<div class="result-greeting">Bienvenue dans l'équipe</div>
<div class="result-badge" id="r-badge"></div>
<div class="result-name" id="r-name"></div>
<div class="result-desc" id="r-desc"></div>
<button class="btn-restart" id="btn-restart">← Recommencer</button>
</div>
</div>
</div>
<script>
// ══════════════════════════════════════════
// DATA
// ══════════════════════════════════════════
const TEAMS = {
SAÏGA: {
img: 'saiga.jpeg',
color: '#f7b731',
desc: "Cerveau en 4D. Trop d'analyse, pas assez de repos.",
members: ['Kawthar','Alexandra','Yoann','Hugo','Tung','Antoine GAB','Aldéric']
},
AXOLOTL: {
img: 'axolotl.jpeg',
color: '#26d0ce',
desc: 'Tu gères ton chaos avec une grâce étrange. Adapté·e, mais à quel prix.',
members: ['Charlène','Lina','Cécile','Philipp','Léon','Alex Clarisse']
},
RATEL: {
img: 'ratel.jpeg',
color: '#ff6b6b',
desc: 'Agent du chaos. Tu fonces, tu vois après. Et souvent… ça passe.',
members: ['Antoine LAMB','Baptiste JAOU','Baptiste JEAN','Astrid','Thimo','Tiphaine','Floriane','Clarisse BA','Marine','Léonie']
},
'POULE DE SOIE': {
img: 'poule-de-soie.jpeg',
color: '#ffd166',
desc: 'Doux·ce, perdu·e, essentiel·le. Tu tiens le groupe sans le savoir.',
members: ['Laurine','Alice','Indra','Zhong','Laurie','Hawa','Margaux','Alexandre P','Thijs']
},
ECHIDNÉ: {
img: 'echidne.jpeg',
color: '#c77dff',
desc: 'Tu veux juste survivre tranquillement. Soft extérieur, panique intérieure.',
members: ['Emma','Clarisse Authier','Christophe','Alexander','Mélanie','Elsa','Samuel','Tes']
}
};
// Build name → team map
const NAME_TO_TEAM = {};
Object.entries(TEAMS).forEach(([teamName, data]) => {
data.members.forEach(m => NAME_TO_TEAM[m] = teamName);
});
const ALL_NAMES = Object.values(TEAMS).flatMap(t => t.members).sort((a,b) => a.localeCompare(b,'fr'));
const QUESTIONS = [
{
text: 'Ton pire cauchemar',
choices: [
'Être coincé·e dans un call Teams sans fin',
'Outlook qui plante juste avant un envoi crucial',
'Une notif Slack à 23h "petite question rapide"',
'Devoir "networker" avec des gens motivés',
"Que quelqu'un dise \"on fait un tour de table\""
]
},
{
text: 'Ta maison à Poudlard',
choices: [
'Serpentard (ambition + fatigue)',
'Poufsouffle (gentil·le mais épuisé·e)',
'Gryffondor (audace discutable)',
'Serdaigle (cerveau en surchauffe)',
'Refuse de choisir, trop de pression'
]
},
{
text: 'Ton plaisir coupable',
choices: [
'Annuler des plans',
'Regarder des apparts hors budget',
'Acheter du matos de sport inutile',
'Dire "je me couche tôt" puis scroller 2h',
'Regarder des gens mieux organisés que toi'
]
},
{
text: 'Ton état un dimanche soir',
choices: [
'Mélancolie élégante',
'Angoisse administrative',
'Déni total',
'Organisation excessive',
'Snack + série + oubli volontaire'
]
},
{
text: 'Quel fromage es-tu ?',
choices: [
'Comté (fiable, apprécié·e, un peu contrôlant·e)',
'Reblochon (réconfortant·e mais intense)',
'Camembert (classique mais imprévisible)',
'Roquefort (clivant·e, puissant·e, assumé·e)',
'Chèvre frais (léger·ère, sensible, un peu fragile)'
]
}
];
// ══════════════════════════════════════════
// STATE
// ══════════════════════════════════════════
let currentName = '';
let currentQ = 0;
let selectedChoice = null;
// ══════════════════════════════════════════
// SCREENS
// ══════════════════════════════════════════
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = document.getElementById(id);
el.classList.add('active');
}
// ══════════════════════════════════════════
// NAME SELECTION
// ══════════════════════════════════════════
function renderNames(filter = '') {
const grid = document.getElementById('names-grid');
const filtered = filter
? ALL_NAMES.filter(n => n.toLowerCase().includes(filter.toLowerCase()))
: ALL_NAMES;
if (!filtered.length) {
grid.innerHTML = '<div class="no-results">Aucun prénom trouvé 😅</div>';
return;
}
grid.innerHTML = filtered.map(name =>
`<button class="name-btn" data-name="${name}">${name}</button>`
).join('');
grid.querySelectorAll('.name-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentName = btn.dataset.name;
startQuiz();
});
});
}
document.getElementById('name-search').addEventListener('input', e => {
renderNames(e.target.value);
});
renderNames();
// ══════════════════════════════════════════
// QUIZ
// ══════════════════════════════════════════
function startQuiz() {
currentQ = 0;
selectedChoice = null;
showScreen('screen-quiz');
renderQuestion();
}
function renderQuestion() {
const q = QUESTIONS[currentQ];
const total = QUESTIONS.length;
document.getElementById('progress-fill').style.width = `${(currentQ / total) * 100}%`;
document.getElementById('q-meta').textContent = `Question ${currentQ + 1} / ${total}`;
document.getElementById('q-text').textContent = q.text;
const letters = ['A','B','C','D','E'];
document.getElementById('q-choices').innerHTML = q.choices.map((c, i) =>
`<button class="choice-btn" data-idx="${i}">
<span class="letter">${letters[i]}</span>
<span>${c}</span>
</button>`
).join('');
selectedChoice = null;
const btnNext = document.getElementById('btn-next');
btnNext.classList.remove('visible');
btnNext.textContent = currentQ < QUESTIONS.length - 1 ? 'Suivant →' : 'Révéler mon équipe ✨';
// Animate in
const choicesEl = document.getElementById('q-choices');
choicesEl.style.opacity = '0';
choicesEl.style.transform = 'translateY(14px)';
setTimeout(() => {
choicesEl.style.transition = 'opacity .3s, transform .3s';
choicesEl.style.opacity = '1';
choicesEl.style.transform = 'translateY(0)';
}, 20);
document.querySelectorAll('.choice-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.choice-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
selectedChoice = parseInt(btn.dataset.idx);
btnNext.classList.add('visible');
});
});
}
document.getElementById('btn-next').addEventListener('click', () => {
if (selectedChoice === null) return;
currentQ++;
if (currentQ < QUESTIONS.length) {
renderQuestion();
} else {
showResult();
}
});
// ══════════════════════════════════════════
// RESULT
// ══════════════════════════════════════════
function showResult() {
const teamName = NAME_TO_TEAM[currentName] || 'AXOLOTL';
const team = TEAMS[teamName];
document.getElementById('progress-fill').style.width = '100%';
const avatar = document.getElementById('r-avatar');
avatar.src = team.img;
avatar.alt = teamName;
const wrap = document.getElementById('r-avatar-wrap');
wrap.style.background = `conic-gradient(${team.color} 0%, ${team.color} 100%)`;
wrap.style.boxShadow = `0 0 28px ${team.color}44`;
document.getElementById('r-name').textContent = `Bienvenue, ${currentName} !`;
const badge = document.getElementById('r-badge');
badge.textContent = teamName;
badge.style.background = team.color + '22';
badge.style.border = `2px solid ${team.color}66`;
badge.style.color = team.color;
document.getElementById('r-desc').textContent = `"${team.desc}"`;
showScreen('screen-result');
launchConfetti(team.color);
}
// ══════════════════════════════════════════
// CONFETTI
// ══════════════════════════════════════════
function launchConfetti(mainColor) {
const container = document.getElementById('confetti');
container.innerHTML = '';
const colors = [mainColor, '#fff', '#ffd166', '#c77dff', '#26d0ce', '#ff6b6b'];
const count = 80;
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.className = 'confetto';
el.style.left = `${Math.random() * 100}vw`;
el.style.width = el.style.height = `${6 + Math.random() * 8}px`;
el.style.borderRadius = Math.random() > .5 ? '50%' : '2px';
el.style.background = colors[Math.floor(Math.random() * colors.length)];
el.style.opacity = (.6 + Math.random() * .4).toString();
const dur = 1.4 + Math.random() * 1.6;
const delay = Math.random() * .8;
el.style.animation = `confettiFall ${dur}s ${delay}s linear forwards`;
container.appendChild(el);
}
setTimeout(() => { container.innerHTML = ''; }, 4000);
}
// ══════════════════════════════════════════
// RESTART
// ══════════════════════════════════════════
document.getElementById('btn-restart').addEventListener('click', () => {
currentName = '';
document.getElementById('name-search').value = '';
renderNames();
showScreen('screen-name');
});
</script>
</body>
</html>