Météo Exam v0.1.0 — calendrier des évaluations en bulletin météo

- API Express + SQLite : classes, évaluations, SSE, auth Bearer
- Front Vite + TS + Tailwind : frise 7 jours, Exam Cards, modales
- Capacitor Android : APK debug, CapacitorHttp natif
- Seed : classe Terminale S2 (code TERMS2)
This commit is contained in:
Mathis 2026-10-02 19:51:13 +02:00
commit 6d154ed5fb
92 changed files with 9574 additions and 0 deletions

View file

@ -0,0 +1,898 @@
<!DOCTYPE html>
<html lang="fr" class="h-full bg-slate-950 text-slate-100 font-sans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Météo Exam - Application Mobile</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts: Nunito -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Nunito', 'sans-serif'],
},
colors: {
appbg: '#0f172a',
cardbg: '#1e293b',
},
boxShadow: {
'ios': '0 20px 40px -15px rgba(0, 0, 0, 0.5)',
'float': '0 10px 30px -5px rgba(239, 68, 68, 0.3)',
}
}
}
}
</script>
<style>
body {
background-color: #020617;
-webkit-tap-highlight-color: transparent;
overscroll-behavior-y: none;
user-select: none;
}
/* Hide Scrollbars */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Mobile Phone Container styling */
.mobile-container {
width: 100%;
max-width: 430px;
height: 100vh;
max-height: 932px;
margin: 0 auto;
background-color: #0f172a;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
@media (min-width: 450px) {
.mobile-container {
height: 90vh;
border-radius: 48px;
margin-top: 5vh;
border: 8px solid #1e293b;
box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.7);
}
}
/* Swipeable Card Styling */
.swipe-container {
position: relative;
height: 140px;
margin-bottom: 1rem;
border-radius: 1.5rem;
overflow: hidden;
}
.swipe-bg {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding: 0 1.5rem;
font-weight: 800;
font-size: 1.1rem;
color: white;
opacity: 0;
transition: opacity 0.2s ease;
border-radius: 1.5rem;
}
.swipe-bg-left { justify-content: flex-end; background-color: #e11d48; }
.swipe-bg-right { justify-content: flex-start; background-color: #10b981; }
.swipe-card {
position: absolute;
inset: 0;
background: #1e293b;
border: 1px solid #334155;
border-radius: 1.5rem;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
touch-action: pan-y;
z-index: 10;
cursor: grab;
will-change: transform;
}
.swipe-card:active { cursor: grabbing; }
/* SVG Map Interactions */
.map-region {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
stroke: #0f172a;
stroke-width: 2.5;
}
.map-region:hover, .map-region.active-region {
filter: brightness(1.25);
stroke: #38bdf8;
stroke-width: 4;
}
/* Tab Switch Animations */
.tab-content {
display: none;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.tab-content.active {
display: flex;
flex-direction: column;
opacity: 1;
transform: translateY(0);
}
/* Pulsing animations */
@keyframes pulseGlow {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
}
.animate-pulse-glow { animation: pulseGlow 3s infinite ease-in-out; }
</style>
</head>
<body class="flex items-center justify-center min-h-screen">
<div class="mobile-container">
<!-- Top Status / Header -->
<header class="pt-8 pb-3 px-6 bg-slate-900/80 backdrop-blur-md flex justify-between items-center z-20 border-b border-slate-800/60">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-2xl bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white font-black shadow-md text-sm">
ME
</div>
<div>
<h1 class="text-lg font-extrabold text-white tracking-tight leading-none">Météo Exam</h1>
<p class="text-xs font-bold text-sky-400 mt-0.5">Terminale S2 • Lycée</p>
</div>
</div>
<div class="flex items-center gap-2">
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-extrabold bg-rose-500/10 text-rose-400 border border-rose-500/20">
<span class="w-2 h-2 rounded-full bg-rose-500 animate-ping mr-1.5"></span>
Direct
</span>
</div>
</header>
<!-- Main Dynamic Scrollable Content -->
<main class="flex-1 overflow-y-auto no-scrollbar p-5 pb-28 relative">
<!-- TAB 1: MÉTÉO (ACCUEIL) -->
<section id="tab-meteo" class="tab-content active space-y-5">
<!-- Giant "Aujourd'hui" Card -->
<div class="relative w-full bg-gradient-to-br from-rose-600 via-orange-600 to-amber-600 rounded-[2.2rem] p-6 text-white shadow-float overflow-hidden border border-rose-400/30">
<!-- Background Glow Effect -->
<div class="absolute -right-8 -bottom-8 w-44 h-44 bg-yellow-300/20 rounded-full blur-2xl"></div>
<div class="flex justify-between items-start relative z-10">
<span class="bg-black/30 backdrop-blur-md text-white px-3.5 py-1 rounded-full text-xs font-black uppercase tracking-wider border border-white/10">
Aujourd'hui • 08h30
</span>
<span class="inline-block text-4xl animate-pulse-glow">🌩️</span>
</div>
<div class="mt-6 relative z-10">
<div class="inline-block bg-rose-950/50 backdrop-blur-sm text-rose-200 text-xs font-black px-3 py-1 rounded-xl mb-2 border border-rose-400/30">
⚠️ ALERTE ROUGE
</div>
<h2 class="text-3xl font-black text-white tracking-tight leading-none">DS de Mathématiques</h2>
<p class="text-amber-100 font-semibold text-sm mt-1.5 opacity-90">Analyse, Intégrales & Géométrie dans l'espace</p>
</div>
<div class="mt-6 pt-4 border-t border-white/15 flex justify-between items-center relative z-10 text-xs font-bold">
<div class="flex items-center gap-2">
<span class="bg-white/20 px-3 py-1.5 rounded-xl"><i class="fa-solid fa-door-open mr-1.5"></i>Salle 204</span>
<span class="bg-white/20 px-3 py-1.5 rounded-xl"><i class="fa-solid fa-clock mr-1.5"></i>2 Hours</span>
</div>
<span class="bg-white text-rose-700 px-3 py-1.5 rounded-xl font-black shadow-sm">Coeff 4.0</span>
</div>
</div>
<!-- Section Title -->
<div class="flex justify-between items-center pt-2">
<h3 class="text-base font-extrabold text-slate-200 uppercase tracking-wider">Prévisions de la semaine</h3>
<span class="text-xs font-bold text-sky-400">7 prochains jours</span>
</div>
<!-- Weekly Forecast Cards -->
<div class="space-y-3">
<!-- Day 1 -->
<div class="bg-slate-800/80 border border-slate-700/60 rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-2xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-2xl">
💨
</div>
<div>
<p class="text-xs font-extrabold text-amber-400 uppercase tracking-wide">Demain • Vendredi</p>
<h4 class="text-base font-bold text-slate-100">Physique-Chimie</h4>
<p class="text-xs text-slate-400 font-semibold">Coup de vent : Interro surprise possible</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-lg bg-slate-700/50 text-slate-300 text-xs font-bold">Coeff 1.0</span>
</div>
<!-- Day 2 -->
<div class="bg-slate-800/80 border border-slate-700/60 rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-2xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-2xl">
☀️
</div>
<div>
<p class="text-xs font-extrabold text-sky-400 uppercase tracking-wide">Lundi 5 Octobre</p>
<h4 class="text-base font-bold text-slate-100">Français & Philosophie</h4>
<p class="text-xs text-slate-400 font-semibold">Grand Soleil : Aucune évaluation répertoriée</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-lg bg-sky-500/10 text-sky-400 text-xs font-bold">Calme</span>
</div>
<!-- Day 3 -->
<div class="bg-slate-800/80 border border-slate-700/60 rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-2xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-2xl">
🌫️
</div>
<div>
<p class="text-xs font-extrabold text-purple-400 uppercase tracking-wide">Mardi 6 Octobre</p>
<h4 class="text-base font-bold text-slate-100">SVT</h4>
<p class="text-xs text-slate-400 font-semibold">Brouillard : Rumeur de TP évalué</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-lg bg-slate-700/50 text-slate-300 text-xs font-bold">Coeff 2.0</span>
</div>
<!-- Day 4 -->
<div class="bg-slate-800/80 border border-slate-700/60 rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/20 flex items-center justify-center text-2xl">
🌩️
</div>
<div>
<p class="text-xs font-extrabold text-rose-400 uppercase tracking-wide">Mercredi 7 Octobre</p>
<h4 class="text-base font-bold text-slate-100">Histoire-Géographie</h4>
<p class="text-xs text-slate-400 font-semibold">Orage : Contrôle sur la Guerre Froide</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-lg bg-slate-700/50 text-slate-300 text-xs font-bold">Coeff 3.0</span>
</div>
</div>
</section>
<!-- TAB 2: RADAR (RUMEURS) -->
<section id="tab-radar" class="tab-content space-y-4">
<!-- Quick Post Card -->
<div class="bg-slate-800 border border-slate-700 rounded-2xl p-3.5 shadow-md">
<div class="flex gap-3">
<div class="w-9 h-9 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold text-sm shrink-0">
<i class="fa-solid fa-bullhorn"></i>
</div>
<input type="text" id="rumor-input" placeholder="Signaler une suspicion d'examen..."
class="w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-2 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 transition-colors">
</div>
<div class="flex justify-between items-center mt-3 pt-2 border-t border-slate-700/50">
<span class="text-xs text-slate-400 font-semibold"><i class="fa-solid fa-circle-info text-sky-400 mr-1"></i>Anonyme pour la classe</span>
<button onclick="postRumor()" class="bg-sky-500 hover:bg-sky-400 text-slate-950 px-4 py-1.5 rounded-xl font-extrabold text-xs shadow-md transition-all active:scale-95">
Publier <i class="fa-solid fa-paper-plane ml-1"></i>
</button>
</div>
</div>
<!-- Swipe Tutorial Banner -->
<div class="bg-indigo-500/10 border border-indigo-500/20 rounded-xl p-3 flex items-center justify-between text-xs text-indigo-300 font-bold">
<span><i class="fa-solid fa-hand-pointer mr-1.5"></i>Glisse vers la <strong>Droite</strong> pour Confirmer, <strong>Gauche</strong> pour Rejeter !</span>
</div>
<!-- Rumors Feed Container -->
<div id="radar-feed" class="space-y-4">
<!-- Swipeable Card 1 -->
<div class="swipe-container" data-id="1">
<div class="swipe-bg swipe-bg-left">
<span><i class="fa-solid fa-xmark mr-2"></i>REJETER</span>
</div>
<div class="swipe-bg swipe-bg-right">
<span>CONFIRMER<i class="fa-solid fa-check ml-2"></i></span>
</div>
<div class="swipe-card">
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse"></span>
<span class="text-xs font-black text-purple-400 uppercase tracking-wider">Matière : Anglais</span>
</div>
<span class="text-xs text-slate-400 font-bold">Il y a 10 min</span>
</div>
<p class="text-sm font-bold text-slate-100 mt-1">« La prof a dit qu'on aurait une compréhension orale surprise cette semaine ! »</p>
<div class="flex justify-between items-center pt-2 border-t border-slate-700/60">
<span class="text-xs text-slate-400 font-semibold">Certitude : <strong class="text-amber-400">68%</strong> (14 votes)</span>
<!-- Fallback Buttons -->
<div class="flex gap-2">
<button onclick="resolveCardManual('1', 'rejected')" class="px-2.5 py-1 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
<button onclick="resolveCardManual('1', 'confirmed')" class="px-2.5 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Swipeable Card 2 -->
<div class="swipe-container" data-id="2">
<div class="swipe-bg swipe-bg-left">
<span><i class="fa-solid fa-xmark mr-2"></i>REJETER</span>
</div>
<div class="swipe-bg swipe-bg-right">
<span>CONFIRMER<i class="fa-solid fa-check ml-2"></i></span>
</div>
<div class="swipe-card">
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-amber-500 animate-pulse"></span>
<span class="text-xs font-black text-amber-400 uppercase tracking-wider">Matière : Philosophie</span>
</div>
<span class="text-xs text-slate-400 font-bold">Il y a 45 min</span>
</div>
<p class="text-sm font-bold text-slate-100 mt-1">« Ramassage des dissertations à la fin de l'heure mardi prochain. »</p>
<div class="flex justify-between items-center pt-2 border-t border-slate-700/60">
<span class="text-xs text-slate-400 font-semibold">Certitude : <strong class="text-emerald-400">85%</strong> (22 votes)</span>
<div class="flex gap-2">
<button onclick="resolveCardManual('2', 'rejected')" class="px-2.5 py-1 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
<button onclick="resolveCardManual('2', 'confirmed')" class="px-2.5 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Swipeable Card 3 -->
<div class="swipe-container" data-id="3">
<div class="swipe-bg swipe-bg-left">
<span><i class="fa-solid fa-xmark mr-2"></i>REJETER</span>
</div>
<div class="swipe-bg swipe-bg-right">
<span>CONFIRMER<i class="fa-solid fa-check ml-2"></i></span>
</div>
<div class="swipe-card">
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></span>
<span class="text-xs font-black text-rose-400 uppercase tracking-wider">Matière : Histoire</span>
</div>
<span class="text-xs text-slate-400 font-bold">Il y a 2h</span>
</div>
<p class="text-sm font-bold text-slate-100 mt-1">« Examen Blanc déplacé au mois prochain d'après le délégué. »</p>
<div class="flex justify-between items-center pt-2 border-t border-slate-700/60">
<span class="text-xs text-slate-400 font-semibold">Certitude : <strong class="text-rose-400">32%</strong> (19 votes)</span>
<div class="flex gap-2">
<button onclick="resolveCardManual('3', 'rejected')" class="px-2.5 py-1 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
<button onclick="resolveCardManual('3', 'confirmed')" class="px-2.5 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- TAB 3: LA CARTE (CARTE MÉTÉO INTERACTIVE) -->
<section id="tab-carte" class="tab-content space-y-4">
<div class="flex justify-between items-center">
<div>
<h2 class="text-lg font-black text-slate-100">Carte Météorologique</h2>
<p class="text-xs font-semibold text-slate-400">Secteurs géographiques par matière</p>
</div>
<span class="px-2.5 py-1 rounded-full bg-sky-500/10 text-sky-400 text-xs font-bold border border-sky-500/20">
Interactive
</span>
</div>
<!-- SVG Interactive Map Container -->
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-3 shadow-lg relative overflow-hidden aspect-[4/3] flex items-center justify-center">
<svg viewBox="0 0 500 400" class="w-full h-full drop-shadow-md">
<!-- Region 1: Maths (Nord / Red Alert) -->
<path id="region-maths" d="M 150,40 Q 250,20 350,50 L 320,160 Q 220,180 160,150 Z"
fill="#dc2626" class="map-region active-region" onclick="selectRegion('maths')" />
<text x="230" y="90" font-size="22" class="pointer-events-none">🌩️</text>
<text x="230" y="120" font-size="12" font-weight="900" fill="#ffffff" text-anchor="middle" class="pointer-events-none">MATHS</text>
<!-- Region 2: SVT (Est / Orange Wind) -->
<path id="region-svt" d="M 350,50 Q 460,80 440,200 L 310,220 Q 300,160 320,160 Z"
fill="#d97706" class="map-region" onclick="selectRegion('svt')" />
<text x="375" y="130" font-size="22" class="pointer-events-none">💨</text>
<text x="375" y="160" font-size="12" font-weight="900" fill="#ffffff" text-anchor="middle" class="pointer-events-none">SVT</text>
<!-- Region 3: Physique (Centre / Purple Fog) -->
<path id="region-physique" d="M 160,150 Q 220,180 310,220 L 280,320 Q 180,310 140,240 Z"
fill="#7c3aed" class="map-region" onclick="selectRegion('physique')" />
<text x="220" y="225" font-size="22" class="pointer-events-none">🌫️</text>
<text x="220" y="255" font-size="12" font-weight="900" fill="#ffffff" text-anchor="middle" class="pointer-events-none">PHYSIQUE</text>
<!-- Region 4: Histoire (Ouest / Calm Sun) -->
<path id="region-histoire" d="M 150,40 L 160,150 Q 140,240 60,220 Q 30,120 150,40 Z"
fill="#0284c7" class="map-region" onclick="selectRegion('histoire')" />
<text x="90" y="130" font-size="22" class="pointer-events-none">☀️️</text>
<text x="90" y="160" font-size="12" font-weight="900" fill="#ffffff" text-anchor="middle" class="pointer-events-none">HISTOIRE</text>
<!-- Region 5: Français (Sud / Calm Sun) -->
<path id="region-francais" d="M 140,240 Q 180,310 280,320 L 260,380 Q 120,390 60,220 Z"
fill="#059669" class="map-region" onclick="selectRegion('francais')" />
<text x="170" y="315" font-size="22" class="pointer-events-none">☀️</text>
<text x="170" y="340" font-size="12" font-weight="900" fill="#ffffff" text-anchor="middle" class="pointer-events-none">FRANÇAIS</text>
</svg>
</div>
<!-- Selected Region Detail Box -->
<div id="region-detail-box" class="bg-slate-800 border border-slate-700 rounded-2xl p-4 transition-all">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2.5">
<span id="region-icon" class="text-3xl">🌩️</span>
<div>
<h3 id="region-title" class="text-base font-black text-slate-100">Secteur : Mathématiques</h3>
<p id="region-status" class="text-xs font-bold text-rose-400">Alerte Rouge • Perturbations Majeures</p>
</div>
</div>
<span id="region-coeff" class="bg-slate-700 text-slate-200 text-xs font-black px-2.5 py-1 rounded-lg">Coeff 4.0</span>
</div>
<p id="region-desc" class="text-xs text-slate-300 font-medium leading-relaxed mt-2 pt-2 border-t border-slate-700/60">
Avis de forte tempête pour la journée d'aujourd'hui. Contrôle continu à fort coefficient programmé. Prévisibilité maximale (98%).
</p>
</div>
</section>
<!-- TAB 4: PLUS (...) (RÉGLAGES ET LÉGENDE) -->
<section id="tab-plus" class="tab-content space-y-5">
<!-- User Profile Card -->
<div class="bg-slate-800/90 border border-slate-700 rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-sky-400 to-indigo-500 text-white flex items-center justify-center font-black text-lg shadow-md">
TS
</div>
<div>
<h3 class="text-base font-black text-slate-100">Thomas S.</h3>
<p class="text-xs text-slate-400 font-semibold">Délégué de classe • TS2</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-400 text-xs font-black border border-emerald-500/20">
En ligne
</span>
</div>
<!-- Toggles / Preferences -->
<div class="bg-slate-800 border border-slate-700 rounded-2xl p-4 space-y-4">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-wider mb-1">Préférences</h3>
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<i class="fa-solid fa-bell text-sky-400 text-base"></i>
<span class="text-sm font-bold text-slate-200">Alertes "Alerte Rouge" Push</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="toggle-notifications" class="sr-only peer" checked onchange="toggleSetting('Notifications Push')">
<div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500"></div>
</label>
</div>
<div class="h-px bg-slate-700/60"></div>
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<i class="fa-solid fa-moon text-indigo-400 text-base"></i>
<span class="text-sm font-bold text-slate-200">Mode Sombre Automatique</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="toggle-darkmode" class="sr-only peer" checked onchange="toggleSetting('Mode Sombre')">
<div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-500"></div>
</label>
</div>
</div>
<!-- Visual Legend Grid -->
<div class="space-y-3">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-wider">Légende Météorologique</h3>
<div class="grid grid-cols-1 gap-2.5">
<div class="bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3">
<span class="text-3xl">☀️</span>
<div>
<h4 class="text-sm font-black text-sky-400">Grand Soleil</h4>
<p class="text-xs text-slate-300 font-medium">Journée calme. Aucun examen répertorié ni suspecté.</p>
</div>
</div>
<div class="bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3">
<span class="text-3xl">🌫️</span>
<div>
<h4 class="text-sm font-black text-purple-400">Brouillard</h4>
<p class="text-xs text-slate-300 font-medium">Zone d'incertitude. Rumeur ou sous-entendu d'un professeur.</p>
</div>
</div>
<div class="bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3">
<span class="text-3xl">💨</span>
<div>
<h4 class="text-sm font-black text-amber-400">Coup de vent</h4>
<p class="text-xs text-slate-300 font-medium">Interrogation surprise potentielle ou petit QCM (Coeff faible).</p>
</div>
</div>
<div class="bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3">
<span class="text-3xl">🌩️️</span>
<div>
<h4 class="text-sm font-black text-rose-400">Orage / Pluie</h4>
<p class="text-xs text-slate-300 font-medium">Devoir Surveillé (DS) confirmé ou évaluation de chapitre.</p>
</div>
</div>
<div class="bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3">
<span class="text-3xl">🚨</span>
<div>
<h4 class="text-sm font-black text-red-500">Alerte Rouge / Tempête</h4>
<p class="text-xs text-slate-300 font-medium">Examen blanc, épreuve commune ou gros devoir de fin de trimestre.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Fixed Bottom Navigation -->
<nav class="absolute bottom-0 left-0 right-0 bg-slate-900/95 backdrop-blur-xl border-t border-slate-800 px-4 py-3 flex justify-around items-center z-30">
<button onclick="switchTab('meteo', this)" class="nav-btn flex flex-col items-center gap-1 text-sky-400 transition-colors">
<i class="fa-solid fa-cloud-sun text-xl"></i>
<span class="text-[10px] font-black tracking-wide">Météo</span>
</button>
<button onclick="switchTab('radar', this)" class="nav-btn flex flex-col items-center gap-1 text-slate-500 hover:text-slate-300 transition-colors">
<div class="relative">
<i class="fa-solid fa-satellite-dish text-xl"></i>
<span class="absolute -top-1 -right-1 w-2 h-2 bg-rose-500 rounded-full"></span>
</div>
<span class="text-[10px] font-black tracking-wide">Radar</span>
</button>
<button onclick="switchTab('carte', this)" class="nav-btn flex flex-col items-center gap-1 text-slate-500 hover:text-slate-300 transition-colors">
<i class="fa-solid fa-map-marked-alt text-xl"></i>
<span class="text-[10px] font-black tracking-wide">La Carte</span>
</button>
<button onclick="switchTab('plus', this)" class="nav-btn flex flex-col items-center gap-1 text-slate-500 hover:text-slate-300 transition-colors">
<i class="fa-solid fa-ellipsis-h text-xl"></i>
<span class="text-[10px] font-black tracking-wide">Plus</span>
</button>
</nav>
<!-- Floating Toast Notification -->
<div id="toast" class="absolute top-20 left-1/2 transform -translate-x-1/2 bg-slate-800 text-slate-100 border border-slate-700 px-4 py-2 rounded-full font-bold text-xs shadow-2xl opacity-0 pointer-events-none transition-all duration-300 z-50 flex items-center gap-2">
<i id="toast-icon" class="fa-solid fa-circle-check text-emerald-400"></i>
<span id="toast-text">Notification</span>
</div>
</div>
<!-- JavaScript Code Section -->
<script>
// Switch Active Tab Function
function switchTab(tabName, btnEl) {
// Hide all tabs
const tabs = document.querySelectorAll('.tab-content');
tabs.forEach(tab => tab.classList.remove('active'));
// Deactivate all nav buttons
const navBtns = document.querySelectorAll('.nav-btn');
navBtns.forEach(btn => {
btn.classList.remove('text-sky-400');
btn.classList.add('text-slate-500');
});
// Show selected tab & highlight button
const targetTab = document.getElementById('tab-' + tabName);
if (targetTab) {
targetTab.classList.add('active');
}
if (btnEl) {
btnEl.classList.remove('text-slate-500');
btnEl.classList.add('text-sky-400');
}
}
// Helper: Toast Notifications
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const toastText = document.getElementById('toast-text');
const toastIcon = document.getElementById('toast-icon');
toastText.innerText = message;
if (type === 'success') {
toastIcon.className = 'fa-solid fa-circle-check text-emerald-400';
} else if (type === 'reject') {
toastIcon.className = 'fa-solid fa-circle-xmark text-rose-400';
} else {
toastIcon.className = 'fa-solid fa-circle-info text-sky-400';
}
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-2');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.remove('opacity-100', 'translate-y-0');
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-2');
}, 2500);
}
// Swipe Card Gesture Class
class SwipeCardController {
constructor(container) {
this.container = container;
this.card = container.querySelector('.swipe-card');
this.bgLeft = container.querySelector('.swipe-bg-left');
this.bgRight = container.querySelector('.swipe-bg-right');
this.startX = 0;
this.currentX = 0;
this.isDragging = false;
this.threshold = 90; // swipe action threshold in px
this.bindEvents();
}
bindEvents() {
const onStart = (x) => {
this.isDragging = true;
this.startX = x;
this.card.style.transition = 'none';
};
const onMove = (x) => {
if (!this.isDragging) return;
this.currentX = x - this.startX;
// Move and rotate card
this.card.style.transform = `translateX(${this.currentX}px) rotate(${this.currentX * 0.04}deg)`;
// Adjust background indicators opacity
if (this.currentX > 0) {
this.bgRight.style.opacity = Math.min(this.currentX / this.threshold, 1);
this.bgLeft.style.opacity = '0';
} else if (this.currentX < 0) {
this.bgLeft.style.opacity = Math.min(Math.abs(this.currentX) / this.threshold, 1);
this.bgRight.style.opacity = '0';
}
};
const onEnd = () => {
if (!this.isDragging) return;
this.isDragging = false;
this.card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
if (this.currentX > this.threshold) {
this.resolve('confirmed');
} else if (this.currentX < -this.threshold) {
this.resolve('rejected');
} else {
this.reset();
}
};
// Touch Listeners
this.card.addEventListener('touchstart', (e) => onStart(e.touches[0].clientX), { passive: true });
this.card.addEventListener('touchmove', (e) => onMove(e.touches[0].clientX), { passive: true });
this.card.addEventListener('touchend', onEnd);
// Mouse Listeners
this.card.addEventListener('mousedown', (e) => onStart(e.clientX));
window.addEventListener('mousemove', (e) => { if (this.isDragging) onMove(e.clientX); });
window.addEventListener('mouseup', () => { if (this.isDragging) onEnd(); });
}
resolve(type) {
const flyOutX = type === 'confirmed' ? 600 : -600;
this.card.style.transform = `translateX(${flyOutX}px) rotate(${flyOutX * 0.04}deg)`;
this.card.style.opacity = '0';
setTimeout(() => {
this.container.style.transition = 'all 0.3s ease';
this.container.style.height = '0px';
this.container.style.marginBottom = '0px';
this.container.style.opacity = '0';
setTimeout(() => {
this.container.remove();
showToast(type === 'confirmed' ? 'Rumeur confirmée !' : 'Rumeur rejetée !', type === 'confirmed' ? 'success' : 'reject');
}, 300);
}, 150);
}
reset() {
this.card.style.transform = 'translateX(0px) rotate(0deg)';
this.bgLeft.style.opacity = '0';
this.bgRight.style.opacity = '0';
}
}
// Manual Card Action (Fallback Buttons)
function resolveCardManual(cardId, action) {
const cardContainer = document.querySelector(`.swipe-container[data-id="${cardId}"]`);
if (cardContainer && cardContainer.controller) {
cardContainer.controller.resolve(action);
}
}
// Data store for map regions
const mapData = {
maths: {
title: "Secteur : Mathématiques",
status: "Alerte Rouge • Perturbations Majeures",
statusClass: "text-rose-400",
icon: "🌩️",
coeff: "Coeff 4.0",
desc: "Avis de forte tempête pour la journée d'aujourd'hui. Devoir surveillé de 2 heures à fort coefficient programmé."
},
svt: {
title: "Secteur : SVT",
status: "Coup de vent • Rafales d'interro",
statusClass: "text-amber-400",
icon: "💨",
coeff: "Coeff 2.0",
desc: "Risque de petit QCM surprise en début d'heure sur le chapitre de la géologie. Soyez vigilants."
},
physique: {
title: "Secteur : Physique-Chimie",
status: "Brouillard • Visibilité réduite",
statusClass: "text-purple-400",
icon: "🌫️",
coeff: "Coeff 2.0",
desc: "Suspicion de TP évalué la semaine prochaine. Rumeurs soutenues par 68% de la classe."
},
histoire: {
title: "Secteur : Histoire-Géographie",
status: "Grand Soleil • Calme Plat",
statusClass: "text-sky-400",
icon: "☀️",
coeff: "Coeff 1.0",
desc: "Conditions idéales. Aucun contrôle prévu d'ici la fin de la semaine."
},
francais: {
title: "Secteur : Français & Philo",
status: "Grand Soleil • Découverte",
statusClass: "text-emerald-400",
icon: "☀️",
coeff: "Coeff 1.0",
desc: "Aucune perturbation à signaler. Cours magistral standard."
}
};
// Region Select Click Event
function selectRegion(subjectKey) {
// Remove active stroke from all regions
document.querySelectorAll('.map-region').forEach(el => el.classList.remove('active-region'));
// Highlight target region
const activePath = document.getElementById('region-' + subjectKey);
if (activePath) {
activePath.classList.add('active-region');
}
// Update detail card
const data = mapData[subjectKey];
if (data) {
document.getElementById('region-icon').innerText = data.icon;
document.getElementById('region-title').innerText = data.title;
document.getElementById('region-status').innerText = data.status;
document.getElementById('region-status').className = `text-xs font-bold ${data.statusClass}`;
document.getElementById('region-coeff').innerText = data.coeff;
document.getElementById('region-desc').innerText = data.desc;
}
}
// Post Rumor Function
function postRumor() {
const input = document.getElementById('rumor-input');
const text = input.value.trim();
if (!text) {
showToast('Veuillez écrire une rumeur', 'info');
return;
}
const feed = document.getElementById('radar-feed');
const newId = Date.now().toString();
const cardHTML = `
<div class="swipe-container" data-id="${newId}">
<div class="swipe-bg swipe-bg-left">
<span><i class="fa-solid fa-xmark mr-2"></i>REJETER</span>
</div>
<div class="swipe-bg swipe-bg-right">
<span>CONFIRMER<i class="fa-solid fa-check ml-2"></i></span>
</div>
<div class="swipe-card">
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-sky-400 animate-pulse"></span>
<span class="text-xs font-black text-sky-400 uppercase tracking-wider">Matière : Général</span>
</div>
<span class="text-xs text-slate-400 font-bold">À l'instant</span>
</div>
<p class="text-sm font-bold text-slate-100 mt-1">« ${text} »</p>
<div class="flex justify-between items-center pt-2 border-t border-slate-700/60">
<span class="text-xs text-slate-400 font-semibold">Certitude : <strong class="text-sky-400">50%</strong> (1 vote)</span>
<div class="flex gap-2">
<button onclick="resolveCardManual('${newId}', 'rejected')" class="px-2.5 py-1 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
<button onclick="resolveCardManual('${newId}', 'confirmed')" class="px-2.5 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-xs font-extrabold transition-colors">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
</div>
</div>
`;
feed.insertAdjacentHTML('afterbegin', cardHTML);
const newContainer = feed.querySelector(`.swipe-container[data-id="${newId}"]`);
newContainer.controller = new SwipeCardController(newContainer);
input.value = '';
showToast('Rumeur publiée avec succès !', 'success');
}
// Toggle Settings Toast
function toggleSetting(name) {
showToast(`Réglage "${name}" mis à jour`, 'info');
}
// Initialize all swipe cards on DOM load
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.swipe-container').forEach(container => {
container.controller = new SwipeCardController(container);
});
});
</script>
</body>
</html>