Compare commits
39 Commits
954c7d2b1a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b818414ed | |||
| f027dfcf4c | |||
| 34216a52f9 | |||
| f611bdcd7e | |||
| f7c846c85c | |||
| a74df51201 | |||
| 42650ca351 | |||
| 21a7359057 | |||
| 6bb7985512 | |||
| 090cbc19a2 | |||
| 794ae69fa0 | |||
| 88b075fb88 | |||
| b5f2eac842 | |||
| a42957ad41 | |||
| 421ae4c2bf | |||
| affa042c05 | |||
| 30817c829b | |||
| 41d8b98f96 | |||
| d251782842 | |||
| 66ed3c505f | |||
| 543bb6cd52 | |||
| a2a4fea004 | |||
| 8164b03cc4 | |||
| d351ee6efb | |||
| 0b4c2244fe | |||
| f720beaf3c | |||
| 2a2ed26563 | |||
| fb5c7e9720 | |||
| a418558393 | |||
| c1bd61c10c | |||
| 317f98b711 | |||
| 2a85ced1e6 | |||
| 44be41b2b2 | |||
| 9140d2f468 | |||
| 75c5a1ac84 | |||
| 1e757ccda8 | |||
| f7f73591c1 | |||
| cf69373417 | |||
| 2227aa89d5 |
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "static-site",
|
||||||
|
"runtimeExecutable": "python3",
|
||||||
|
"runtimeArgs": ["-m", "http.server", "8123"],
|
||||||
|
"port": 8123
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Contient le mot de passe de la base SQL Server — à créer/éditer uniquement sur
|
||||||
|
# le serveur bsite.net à partir de web.config.example (ne jamais versionner)
|
||||||
|
exercices/memory-pictogrammes/api/web.config
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Index Documentation Technique & Biologie | NextGN Formation</title>
|
<title>Index Documentation Technique & Biologie | Gauthier Chombart & Nathan Chauwin</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -506,7 +506,7 @@
|
|||||||
|
|
||||||
<nav class="nav-header">
|
<nav class="nav-header">
|
||||||
<a href="index.html" class="header-logo">
|
<a href="index.html" class="header-logo">
|
||||||
<span>NextGN</span> Formation
|
Gauthier Chombart & Nathan Chauwin
|
||||||
</a>
|
</a>
|
||||||
<div class="header-nav">
|
<div class="header-nav">
|
||||||
<a href="index.html#formations">Exercices interactifs</a>
|
<a href="index.html#formations">Exercices interactifs</a>
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation - Index Documentation Technique & Biologie</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p>Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
<p>Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
BIN
data/ghs05-corrosif.jpg
Normal file
BIN
data/ghs05-corrosif.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
data/ghs07-nocif.jpg
Normal file
BIN
data/ghs07-nocif.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
data/ghs09-environnement.jpg
Normal file
BIN
data/ghs09-environnement.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
padding-top: calc(70px + 2rem);
|
padding-top: calc(70px + 2rem);
|
||||||
padding-bottom: calc(52px + 2rem);
|
padding-bottom: calc(84px + 2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.4s ease;
|
transition: opacity 0.4s ease;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -38,73 +38,67 @@
|
|||||||
transition: width 0.4s ease;
|
transition: width 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PHASE INDICATOR */
|
|
||||||
.phase-badge {
|
|
||||||
position: fixed;
|
|
||||||
top: 80px; right: 16px;
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BOTTOM BAR */
|
/* BOTTOM BAR */
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0; left: 0; right: 0;
|
bottom: 0; left: 0; right: 0;
|
||||||
height: 52px;
|
height: 84px;
|
||||||
background: rgba(250, 238, 218, 0.98);
|
background: rgba(250, 238, 218, 0.98);
|
||||||
border-top: 2px solid var(--amber);
|
border-top: 2px solid var(--amber);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
.bottom-actions {
|
||||||
/* SLIDE NAV */
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footer-btn {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 11px 24px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1.5px solid var(--amber);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.footer-btn:hover { transform: translateY(-1px); }
|
||||||
|
.footer-btn:active { transform: scale(0.98); }
|
||||||
|
.footer-btn.is-primary { background: var(--amber); color: #fff; }
|
||||||
|
.footer-btn.is-primary:hover { background: var(--amber-dark); }
|
||||||
|
.footer-btn.is-secondary { background: transparent; color: var(--amber-dark); }
|
||||||
|
.footer-btn.is-secondary:hover { background: rgba(186,117,23,0.08); }
|
||||||
.slide-counter {
|
.slide-counter {
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--amber-dark);
|
color: var(--amber-dark);
|
||||||
}
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
/* BACK BUTTON */
|
.slide-counter { display: none; }
|
||||||
.back-btn {
|
|
||||||
background: none;
|
|
||||||
border: 0.5px solid var(--amber);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--amber-dark);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-family: 'DM Sans', sans-serif;
|
|
||||||
}
|
}
|
||||||
.back-btn:hover { background: rgba(186,117,23,0.08); }
|
|
||||||
|
|
||||||
/* FORMATEUR SELECTOR */
|
/* FORMATEUR SELECTOR */
|
||||||
.formateur-btn {
|
.formateur-btn {
|
||||||
font-family: 'Syne', sans-serif;
|
flex: 1; font-family: 'DM Sans', sans-serif; font-size: 13.5px;
|
||||||
font-size: 14px;
|
padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px;
|
||||||
font-weight: 600;
|
background: var(--bg); color: var(--text); cursor: pointer; transition: all 0.2s ease;
|
||||||
padding: 10px 24px;
|
|
||||||
border-radius: 30px;
|
|
||||||
border: 1.5px solid var(--border);
|
|
||||||
background: white;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
.formateur-btn:hover { border-color: var(--amber); color: var(--amber); }
|
.formateur-btn:hover { border-color: var(--amber); }
|
||||||
.formateur-btn.selected { background: var(--amber-light); border-color: var(--amber); color: var(--amber-dark); font-weight: 700; }
|
.formateur-btn.selected { background: var(--amber); border-color: var(--amber); color: #fff; }
|
||||||
|
|
||||||
/* CARDS & BUTTONS */
|
/* CARDS & BUTTONS */
|
||||||
.btn {
|
.btn {
|
||||||
@@ -246,40 +240,42 @@
|
|||||||
.q-item { flex-direction: column; align-items: stretch; }
|
.q-item { flex-direction: column; align-items: stretch; }
|
||||||
.q-item .q-head { display: flex; align-items: flex-start; gap: 12px; }
|
.q-item .q-head { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
|
||||||
/* IDENTIFICATION SCREEN */
|
/* IDENTIFICATION SCREEN — carte modèle BioCID / FDS */
|
||||||
.id-form {
|
.identity-card {
|
||||||
|
background: white;
|
||||||
|
border: 0.5px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
max-width: 460px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
margin: 1.25rem auto;
|
||||||
display: flex;
|
text-align: left;
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
margin: 1.25rem 0;
|
|
||||||
}
|
}
|
||||||
.id-field label {
|
.identity-card .identity-title {
|
||||||
display: block;
|
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--text-muted);
|
color: var(--amber-dark);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 10px;
|
||||||
text-align: left;
|
display: flex; align-items: center; gap: 6px;
|
||||||
}
|
}
|
||||||
.id-field input {
|
.identity-fields { display: flex; flex-direction: column; gap: 8px; }
|
||||||
width: 100%;
|
.identity-fields input {
|
||||||
font-family: 'DM Sans', sans-serif;
|
font-family: 'DM Sans', sans-serif;
|
||||||
font-size: 15px;
|
font-size: 13.5px;
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1.5px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: white;
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.id-field input:focus {
|
.identity-fields input:focus { outline: none; border-color: var(--amber); background: white; }
|
||||||
outline: none;
|
.formateur-label { font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted); margin: 4px 0 2px; }
|
||||||
border-color: var(--amber);
|
.formateur-row { display: flex; gap: 8px; }
|
||||||
}
|
|
||||||
|
|
||||||
/* TABLE FORM CONTROLS */
|
/* TABLE FORM CONTROLS */
|
||||||
.risk-table input, .risk-table textarea {
|
.risk-table input, .risk-table textarea {
|
||||||
@@ -496,6 +492,19 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
.info-card {
|
||||||
|
background: var(--amber-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
max-width: 620px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
.steps-row {
|
.steps-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -574,7 +583,7 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem auto;
|
||||||
}
|
}
|
||||||
.final-card {
|
.final-card {
|
||||||
background: white;
|
background: white;
|
||||||
@@ -606,7 +615,7 @@
|
|||||||
/* SCROLLABLE AREA */
|
/* SCROLLABLE AREA */
|
||||||
.scroll-area {
|
.scroll-area {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(100vh - 232px);
|
max-height: calc(100vh - 200px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -633,7 +642,7 @@
|
|||||||
|
|
||||||
<!-- HEADER WITH NAVIGATION -->
|
<!-- HEADER WITH NAVIGATION -->
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="../index.html">Accueil</a>
|
<a href="../index.html">Accueil</a>
|
||||||
<a href="../index.html#formations">Exercices</a>
|
<a href="../index.html#formations">Exercices</a>
|
||||||
@@ -647,13 +656,14 @@
|
|||||||
<!-- Progress bar -->
|
<!-- Progress bar -->
|
||||||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||||||
|
|
||||||
<!-- Phase badge -->
|
|
||||||
<div class="phase-badge" id="phaseBadge" style="display:none"></div>
|
|
||||||
|
|
||||||
<!-- Bottom bar -->
|
<!-- Bottom bar -->
|
||||||
<div class="bottom-bar" id="bottomBar" style="display:none;">
|
<div class="bottom-bar" id="bottomBar" style="display:none;">
|
||||||
<button class="back-btn" id="backBtn" onclick="goBack()" style="display:none;">← Retour</button>
|
|
||||||
<div class="slide-counter" id="slideCounter"></div>
|
<div class="slide-counter" id="slideCounter"></div>
|
||||||
|
<div class="bottom-actions">
|
||||||
|
<button class="footer-btn is-secondary" id="backBtn" onclick="goBack()" style="display:none;">← Retour</button>
|
||||||
|
<button class="footer-btn is-secondary" id="footerHintBtn" onclick="showHintCurrent()" style="display:none;">💡 Besoin d'aide ?</button>
|
||||||
|
<button class="footer-btn is-primary" id="footerNextBtn" onclick="goNext()" style="display:none;">Étape suivante →</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal overlay -->
|
<!-- Modal overlay -->
|
||||||
@@ -666,48 +676,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 0 — IDENTIFICATION ======================== -->
|
<!-- ======================== SLIDE 0 — ACCUEIL ======================== -->
|
||||||
<div class="slide active fade-in" id="slide-0">
|
<div class="slide active fade-in" id="slide-0">
|
||||||
<div class="deco-circle" style="width:320px;height:320px;background:var(--amber-light);top:-80px;left:-80px;opacity:0.5;"></div>
|
<div class="deco-circle" style="width:400px;height:400px;background:var(--amber-light);top:-100px;right:-80px;opacity:0.6;"></div>
|
||||||
<div style="text-align:center; z-index:1; width:100%;">
|
<div class="deco-circle" style="width:200px;height:200px;background:var(--amber-light);bottom:60px;left:-60px;opacity:0.5;"></div>
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<div class="hero-label">Certibiocide — Travaux pratiques</div>
|
||||||
|
<div class="hero-title">Étude de cas<br>Nuisibles</div>
|
||||||
|
<p class="hero-sub">Votre mission, si vous l'acceptez : maîtriser l'identification, le traitement et la sécurité face aux nuisibles.</p>
|
||||||
|
<button class="btn btn-amber" onclick="goTo(1)">Démarrer la mission →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================== SLIDE 1 — CONTEXTE & IDENTIFICATION ======================== -->
|
||||||
|
<div class="slide" id="slide-1">
|
||||||
|
<div class="deco-circle" style="width:300px;height:300px;background:var(--amber-light);top:-60px;left:-60px;opacity:0.4;"></div>
|
||||||
<div class="section-tag" style="background:var(--amber-light);color:var(--amber-dark);">Avant de commencer</div>
|
<div class="section-tag" style="background:var(--amber-light);color:var(--amber-dark);">Avant de commencer</div>
|
||||||
<div class="slide-title">Identification du groupe</div>
|
<div class="slide-title">Contexte & accès</div>
|
||||||
<p class="slide-intro" style="margin:0.5rem auto 0;max-width:420px;">Ces informations permettront au formateur de retrouver vos réponses.</p>
|
<p class="slide-intro">3 travaux pratiques vous attendent : protocole d'intervention, recherche produit sur BioCID, puis analyse de risques. Ces informations permettront au formateur de retrouver vos réponses.</p>
|
||||||
<div class="id-form" style="margin-left:auto;margin-right:auto;">
|
<div class="info-card" style="max-width:620px;">
|
||||||
<div class="id-field">
|
<span>🎯</span>
|
||||||
<label for="inputNom">Nom du stagiaire ou du groupe</label>
|
<div>Vous allez définir un <strong>protocole d'intervention</strong>, trouver votre <strong>produit sur BioCID</strong>, puis réaliser l'<strong>analyse de risques</strong>.</div>
|
||||||
<input type="text" id="inputNom" placeholder="Ex : Groupe 3 - Dupont / Martin" autocomplete="off">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="id-field">
|
<div class="identity-card" style="max-width:620px;margin-top:0.5rem;">
|
||||||
<label for="inputDate">Date de la session</label>
|
<div class="identity-title">👥 Identification du groupe</div>
|
||||||
|
<div class="identity-fields">
|
||||||
|
<input type="text" id="inputNom" placeholder="Nom du groupe / stagiaires (ex : Groupe 3 — Dupont / Martin)" autocomplete="off">
|
||||||
<input type="date" id="inputDate">
|
<input type="date" id="inputDate">
|
||||||
</div>
|
<div class="formateur-label">Formateur de la session</div>
|
||||||
<div class="id-field">
|
<div class="formateur-row">
|
||||||
<label>Formateur de la session</label>
|
|
||||||
<div style="display:flex;gap:10px;margin-top:2px;">
|
|
||||||
<button type="button" class="formateur-btn" id="btnNathan" onclick="selectFormateur('Nathan')">Nathan</button>
|
<button type="button" class="formateur-btn" id="btnNathan" onclick="selectFormateur('Nathan')">Nathan</button>
|
||||||
<button type="button" class="formateur-btn" id="btnGauthier" onclick="selectFormateur('Gauthier')">Gauthier</button>
|
<button type="button" class="formateur-btn" id="btnGauthier" onclick="selectFormateur('Gauthier')">Gauthier</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="idError" style="color:var(--coral);font-size:13px;margin-top:8px;display:none;">Merci de renseigner au moins le nom du groupe.</div>
|
||||||
<div id="idError" style="color:var(--coral);font-size:13px;margin-bottom:0.5rem;display:none;">Merci de renseigner au moins le nom du groupe.</div>
|
|
||||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">
|
|
||||||
<button class="btn btn-teal" onclick="confirmIdentite()">Continuer →</button>
|
|
||||||
<a href="correction-tp-nuisibles.html" class="btn btn-outline btn-teal">Corriger</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ======================== SLIDE 1 — ACCUEIL ======================== -->
|
|
||||||
<div class="slide" id="slide-1">
|
|
||||||
<div class="deco-circle" style="width:400px;height:400px;background:var(--amber-light);top:-100px;right:-80px;opacity:0.6;"></div>
|
|
||||||
<div class="deco-circle" style="width:200px;height:200px;background:var(--amber-light);bottom:60px;left:-60px;opacity:0.5;"></div>
|
|
||||||
<div style="text-align:center; z-index:1;">
|
|
||||||
<div class="hero-label">Formation</div>
|
|
||||||
<div class="hero-title">CERTIBIOCIDE</div>
|
|
||||||
<div style="font-family:'Syne',sans-serif;font-size:clamp(14px,2.5vw,22px);font-weight:600;color:var(--amber-mid);letter-spacing:0.04em;margin-bottom:1.5rem;">TRAVAUX PRATIQUES</div>
|
|
||||||
<p class="hero-sub">Votre mission, si vous l'acceptez : maîtriser l'identification, le traitement et la sécurité face aux nuisibles.</p>
|
|
||||||
<button class="btn btn-teal" onclick="goTo(2)">Démarrer la mission →</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -715,8 +717,12 @@
|
|||||||
<div class="slide" id="slide-2">
|
<div class="slide" id="slide-2">
|
||||||
<div class="deco-circle" style="width:300px;height:300px;background:var(--amber-light);top:-60px;left:-60px;opacity:0.4;"></div>
|
<div class="deco-circle" style="width:300px;height:300px;background:var(--amber-light);top:-60px;left:-60px;opacity:0.4;"></div>
|
||||||
<div class="section-tag" style="background:var(--amber-light);color:var(--amber-dark);">Étape 1 / 3</div>
|
<div class="section-tag" style="background:var(--amber-light);color:var(--amber-dark);">Étape 1 / 3</div>
|
||||||
<div class="slide-title">Choisissez votre mission</div>
|
<div class="slide-title">🎯 Choisissez votre mission</div>
|
||||||
<p class="slide-intro">Chaque groupe choisit un cas pratique. Vous travaillerez avec ce scénario tout au long des 3 exercices.</p>
|
<p class="slide-intro">Chaque groupe choisit un cas pratique. Vous travaillerez avec ce scénario tout au long des 3 exercices.</p>
|
||||||
|
<div class="info-card" style="margin-bottom:1.25rem;">
|
||||||
|
<span>💡</span>
|
||||||
|
<div>Le cas retenu conditionne <strong>tous les exercices suivants</strong> et apparaît dans le PDF final. Choisissez celui qui se rapproche le plus de votre pratique.</div>
|
||||||
|
</div>
|
||||||
<div class="case-grid">
|
<div class="case-grid">
|
||||||
<div class="case-card" onclick="selectCase('punaises','🛏️','Hôtel — Punaises de lit','Infestation importante dans un hôtel')">
|
<div class="case-card" onclick="selectCase('punaises','🛏️','Hôtel — Punaises de lit','Infestation importante dans un hôtel')">
|
||||||
<div class="case-icon">🛏️</div>
|
<div class="case-icon">🛏️</div>
|
||||||
@@ -744,9 +750,6 @@
|
|||||||
<div class="case-sub">Camping</div>
|
<div class="case-sub">Camping</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="btn btn-teal" id="btnConfirmCase" onclick="goTo(3)" style="display:none">Confirmer le choix →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 3 — CONSIGNES ======================== -->
|
<!-- ======================== SLIDE 3 — CONSIGNES ======================== -->
|
||||||
@@ -774,7 +777,6 @@
|
|||||||
<span>💡</span>
|
<span>💡</span>
|
||||||
<div>À tout moment, cliquez sur <strong>« Besoin d'aide ? »</strong> pour accéder à un indice. Le formateur corrigera à l'oral en fin de séance.</div>
|
<div>À tout moment, cliquez sur <strong>« Besoin d'aide ? »</strong> pour accéder à un indice. Le formateur corrigera à l'oral en fin de séance.</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-teal" onclick="goTo(4)">Commencer le TP 1 →</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 4 — TP1 INTRO ======================== -->
|
<!-- ======================== SLIDE 4 — TP1 INTRO ======================== -->
|
||||||
@@ -792,7 +794,6 @@
|
|||||||
<div class="step-pill">2. Intervention</div>
|
<div class="step-pill">2. Intervention</div>
|
||||||
<div class="step-pill">3. Évaluation</div>
|
<div class="step-pill">3. Évaluation</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-amber" onclick="goTo(5)">Commencer →</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 5 — TP1 PREVENTION ======================== -->
|
<!-- ======================== SLIDE 5 — TP1 PREVENTION ======================== -->
|
||||||
@@ -833,10 +834,6 @@
|
|||||||
<textarea class="answer-field" id="ans_prevention_4" placeholder="Votre réponse…" data-question="Éléments observés / preuve de présence du nuisible"></textarea>
|
<textarea class="answer-field" id="ans_prevention_4" placeholder="Votre réponse…" data-question="Éléments observés / preuve de présence du nuisible"></textarea>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('prevention')">💡 Besoin d'aide ?</button>
|
|
||||||
<button class="btn btn-amber" onclick="goTo(6)">Étape suivante →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -871,10 +868,6 @@
|
|||||||
<textarea class="answer-field" id="ans_intervention_3" placeholder="Votre réponse…" data-question="Type de produit (TP) et mode de traitement"></textarea>
|
<textarea class="answer-field" id="ans_intervention_3" placeholder="Votre réponse…" data-question="Type de produit (TP) et mode de traitement"></textarea>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('intervention')">💡 Besoin d'aide ?</button>
|
|
||||||
<button class="btn btn-amber" onclick="goTo(7)">Étape suivante →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -905,10 +898,6 @@
|
|||||||
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:680px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.75rem 0;">
|
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:680px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.75rem 0;">
|
||||||
✅ <strong>TP 1 terminé !</strong> Préparez-vous à identifier le produit adapté à votre cas sur biocid-anses.fr.
|
✅ <strong>TP 1 terminé !</strong> Préparez-vous à identifier le produit adapté à votre cas sur biocid-anses.fr.
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('evaluation')">💡 Besoin d'aide ?</button>
|
|
||||||
<button class="btn btn-coral" onclick="goTo(8)">Passer au TP 2 →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -930,7 +919,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="https://biocid-anses.fr" target="_blank" class="resource-link">Ouvrir le site ↗</a>
|
<a href="https://biocid-anses.fr" target="_blank" class="resource-link">Ouvrir le site ↗</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-coral" onclick="goTo(9)">Commencer →</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 9 — TP2 FDS & AMM ======================== -->
|
<!-- ======================== SLIDE 9 — TP2 FDS & AMM ======================== -->
|
||||||
@@ -971,10 +959,6 @@
|
|||||||
<textarea class="answer-field" id="ans_fds_4" placeholder="Votre réponse…" data-question="Risques pour la santé"></textarea>
|
<textarea class="answer-field" id="ans_fds_4" placeholder="Votre réponse…" data-question="Risques pour la santé"></textarea>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('fds')">💡 Besoin d'aide ?</button>
|
|
||||||
<button class="btn btn-coral" onclick="goTo(10)">Partie suivante →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1019,10 +1003,6 @@
|
|||||||
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:680px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.75rem 0;">
|
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:680px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.75rem 0;">
|
||||||
✅ <strong>TP 2 terminé !</strong> Passez maintenant à l'analyse de risques complète.
|
✅ <strong>TP 2 terminé !</strong> Passez maintenant à l'analyse de risques complète.
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('epi')">💡 Besoin d'aide ?</button>
|
|
||||||
<button class="btn btn-purple" onclick="goTo(11)">Passer au TP 3 →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1039,9 +1019,6 @@
|
|||||||
<div class="pyr-level" style="width:320px;background:#D85A30;">Communication</div>
|
<div class="pyr-level" style="width:320px;background:#D85A30;">Communication</div>
|
||||||
<div class="pyr-level" style="width:360px;background:#BA7517;">EPI (dernier recours)</div>
|
<div class="pyr-level" style="width:360px;background:#BA7517;">EPI (dernier recours)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row" style="margin-top:1rem;">
|
|
||||||
<button class="btn btn-purple" onclick="goTo(12)">Remplir le tableau →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== SLIDE 12 — TP3 TABLEAU ======================== -->
|
<!-- ======================== SLIDE 12 — TP3 TABLEAU ======================== -->
|
||||||
@@ -1110,10 +1087,6 @@
|
|||||||
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:780px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.25rem 0;">
|
<div style="background:var(--amber-light);border-radius:12px;padding:14px 18px;max-width:780px;width:100%;font-size:13px;color:var(--amber-dark);margin:0.25rem 0;">
|
||||||
✅ <strong>TP 3 terminé !</strong> Présentez vos réponses au formateur pour la correction.
|
✅ <strong>TP 3 terminé !</strong> Présentez vos réponses au formateur pour la correction.
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="hint-btn" onclick="showHint('analyse')">💡 Aide générale</button>
|
|
||||||
<button class="btn btn-blue" onclick="goTo(13)">Terminer la mission →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1143,15 +1116,10 @@
|
|||||||
<div class="final-card-sub">Analyse de risques</div>
|
<div class="final-card-sub">Analyse de risques</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="submit-zone">
|
<div class="actions-row">
|
||||||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
|
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
|
||||||
<button class="btn btn-teal" onclick="exporterPDF()">⬇ Télécharger en PDF</button>
|
<a href="corriger-exercice.html?exo=tp" class="btn btn-outline btn-teal">✔ Corriger un rendu</a>
|
||||||
<a href="correction-tp-nuisibles.html" class="btn btn-outline btn-teal">Corriger</a>
|
<button class="btn btn-outline btn-teal" onclick="restartAll()">↺ Recommencer</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:0.75rem;">
|
|
||||||
<a href="https://biocid-anses.fr" target="_blank" class="btn btn-outline btn-teal" style="font-size:13px;padding:10px 20px;">biocid-anses.fr ↗</a>
|
|
||||||
<button class="btn btn-teal" onclick="restartAll()">↺ Recommencer depuis le début</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1164,6 +1132,22 @@ const totalSlides = 13;
|
|||||||
const selectedCase = { key: '', icon: '', title: '', desc: '' };
|
const selectedCase = { key: '', icon: '', title: '', desc: '' };
|
||||||
const stagiaire = { nom: '', date: '', formateur: '' };
|
const stagiaire = { nom: '', date: '', formateur: '' };
|
||||||
|
|
||||||
|
// Configuration des boutons du footer par slide (modèle BioCID)
|
||||||
|
const slideConfig = {
|
||||||
|
1: { next: 2, nextLabel: 'Continuer →' },
|
||||||
|
2: { next: 3, nextLabel: 'Confirmer le choix →' },
|
||||||
|
3: { next: 4, nextLabel: 'Commencer le TP 1 →' },
|
||||||
|
4: { next: 5, nextLabel: 'Commencer →' },
|
||||||
|
5: { next: 6, nextLabel: 'Étape suivante →', hint: 'prevention' },
|
||||||
|
6: { next: 7, nextLabel: 'Étape suivante →', hint: 'intervention' },
|
||||||
|
7: { next: 8, nextLabel: 'Passer au TP 2 →', hint: 'evaluation' },
|
||||||
|
8: { next: 9, nextLabel: 'Commencer →' },
|
||||||
|
9: { next: 10, nextLabel: 'Partie suivante →', hint: 'fds' },
|
||||||
|
10: { next: 11, nextLabel: 'Passer au TP 3 →', hint: 'epi' },
|
||||||
|
11: { next: 12, nextLabel: 'Remplir le tableau →' },
|
||||||
|
12: { next: 13, nextLabel: 'Terminer la mission →', hint: 'analyse' }
|
||||||
|
};
|
||||||
|
|
||||||
const hints = {
|
const hints = {
|
||||||
prevention: {
|
prevention: {
|
||||||
icon: '💡',
|
icon: '💡',
|
||||||
@@ -1250,11 +1234,28 @@ function goTo(n) {
|
|||||||
currentSlide = n;
|
currentSlide = n;
|
||||||
next.style.display = 'flex';
|
next.style.display = 'flex';
|
||||||
setTimeout(() => { next.classList.add('active', 'fade-in'); }, 10);
|
setTimeout(() => { next.classList.add('active', 'fade-in'); }, 10);
|
||||||
|
const sa = next.querySelector('.scroll-area');
|
||||||
|
if (sa) sa.scrollTop = 0;
|
||||||
if (n === 13) setTimeout(launchConfetti, 200);
|
if (n === 13) setTimeout(launchConfetti, 200);
|
||||||
updateUI();
|
updateUI();
|
||||||
updateCaseBanners();
|
updateCaseBanners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
const cfg = slideConfig[currentSlide];
|
||||||
|
if (!cfg || cfg.next == null) return;
|
||||||
|
// Slide 1 : validation du nom du groupe avant de continuer
|
||||||
|
if (currentSlide === 1) { confirmIdentite(); return; }
|
||||||
|
// Slide 2 : un cas doit être sélectionné
|
||||||
|
if (currentSlide === 2 && !selectedCase.key) return;
|
||||||
|
goTo(cfg.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHintCurrent() {
|
||||||
|
const cfg = slideConfig[currentSlide];
|
||||||
|
if (cfg && cfg.hint) showHint(cfg.hint);
|
||||||
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (history.length === 0) return;
|
if (history.length === 0) return;
|
||||||
const prev = history.pop();
|
const prev = history.pop();
|
||||||
@@ -1279,37 +1280,31 @@ function updateUI() {
|
|||||||
if (bottomBar) bottomBar.style.display = currentSlide === 0 ? 'none' : 'flex';
|
if (bottomBar) bottomBar.style.display = currentSlide === 0 ? 'none' : 'flex';
|
||||||
|
|
||||||
const backBtn = document.getElementById('backBtn');
|
const backBtn = document.getElementById('backBtn');
|
||||||
backBtn.style.display = history.length > 0 ? 'block' : 'none';
|
backBtn.style.display = history.length > 0 ? 'inline-flex' : 'none';
|
||||||
|
|
||||||
const badge = document.getElementById('phaseBadge');
|
const cfg = slideConfig[currentSlide] || {};
|
||||||
if (currentSlide === 0 || currentSlide === 1 || currentSlide === 2 || currentSlide === 3 || currentSlide === 13) {
|
const hintBtn = document.getElementById('footerHintBtn');
|
||||||
badge.style.display = 'none';
|
const nextBtn = document.getElementById('footerNextBtn');
|
||||||
} else if (currentSlide >= 4 && currentSlide <= 7) {
|
hintBtn.style.display = cfg.hint ? 'inline-flex' : 'none';
|
||||||
badge.style.display = 'block';
|
if (cfg.next != null) {
|
||||||
badge.style.background = 'var(--amber-light)';
|
// Slide 2 : le bouton n'apparaît qu'une fois le cas choisi
|
||||||
badge.style.color = 'var(--amber-dark)';
|
const gated = currentSlide === 2 && !selectedCase.key;
|
||||||
badge.textContent = 'TP 1 — Protocole';
|
nextBtn.style.display = gated ? 'none' : 'inline-flex';
|
||||||
} else if (currentSlide >= 8 && currentSlide <= 10) {
|
nextBtn.textContent = cfg.nextLabel;
|
||||||
badge.style.display = 'block';
|
} else {
|
||||||
badge.style.background = 'var(--coral-light)';
|
nextBtn.style.display = 'none';
|
||||||
badge.style.color = 'var(--coral)';
|
|
||||||
badge.textContent = 'TP 2 — Produit';
|
|
||||||
} else if (currentSlide >= 11 && currentSlide <= 12) {
|
|
||||||
badge.style.display = 'block';
|
|
||||||
badge.style.background = 'var(--amber-light)';
|
|
||||||
badge.style.color = 'var(--amber-dark)';
|
|
||||||
badge.textContent = 'TP 3 — Risques';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCase(key, icon, title, desc) {
|
function selectCase(key, icon, title, desc) {
|
||||||
|
marquerModifie();
|
||||||
selectedCase.key = key;
|
selectedCase.key = key;
|
||||||
selectedCase.icon = icon;
|
selectedCase.icon = icon;
|
||||||
selectedCase.title = title;
|
selectedCase.title = title;
|
||||||
selectedCase.desc = desc;
|
selectedCase.desc = desc;
|
||||||
document.querySelectorAll('.case-card').forEach(c => c.classList.remove('selected'));
|
document.querySelectorAll('.case-card').forEach(c => c.classList.remove('selected'));
|
||||||
event.currentTarget.classList.add('selected');
|
event.currentTarget.classList.add('selected');
|
||||||
document.getElementById('btnConfirmCase').style.display = 'inline-flex';
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCaseBanners() {
|
function updateCaseBanners() {
|
||||||
@@ -1379,7 +1374,7 @@ function confirmIdentite() {
|
|||||||
document.getElementById('idError').style.display = 'none';
|
document.getElementById('idError').style.display = 'none';
|
||||||
stagiaire.nom = nom;
|
stagiaire.nom = nom;
|
||||||
stagiaire.date = date || new Date().toISOString().slice(0, 10);
|
stagiaire.date = date || new Date().toISOString().slice(0, 10);
|
||||||
goTo(1);
|
goTo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================== TABLEAU TP3 — LIGNES DYNAMIQUES ======================== */
|
/* ======================== TABLEAU TP3 — LIGNES DYNAMIQUES ======================== */
|
||||||
@@ -1606,6 +1601,156 @@ function escapeHtml(str) {
|
|||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================== SAUVEGARDE AUTOMATIQUE ======================== */
|
||||||
|
/* Les réponses sont enregistrées dans le navigateur au fil de la saisie, puis
|
||||||
|
proposées à la restauration au rechargement. En file://, localStorage peut être
|
||||||
|
indisponible (Safari) : dans ce cas la page fonctionne normalement, sans filet. */
|
||||||
|
const CLE_SAUVEGARDE = 'certibiocide:tp-nuisibles:v1';
|
||||||
|
|
||||||
|
let reponsesModifiees = false;
|
||||||
|
function marquerModifie() { reponsesModifiees = true; sauvegarder(); }
|
||||||
|
|
||||||
|
document.addEventListener('input', marquerModifie, true);
|
||||||
|
document.addEventListener('change', marquerModifie, true);
|
||||||
|
|
||||||
|
// Avertit avant tout rechargement ou départ. Le texte de la boîte est imposé
|
||||||
|
// par le navigateur, il n'est pas personnalisable.
|
||||||
|
window.addEventListener('beforeunload', e => {
|
||||||
|
if (!reponsesModifiees) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function etatCourant() {
|
||||||
|
const champs = {};
|
||||||
|
document.querySelectorAll('input[id], textarea[id]').forEach(el => {
|
||||||
|
if (el.value) champs[el.id] = el.value;
|
||||||
|
});
|
||||||
|
// Les cellules du tableau TP3 n'ont pas d'id : on les repère par leur position.
|
||||||
|
const risques = Array.from(document.querySelectorAll('#riskTableBody .risk-row'))
|
||||||
|
.map(row => Array.from(row.querySelectorAll('.risk-cell')).map(c => c.value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
savedAt: new Date().toISOString(),
|
||||||
|
slide: currentSlide,
|
||||||
|
champs,
|
||||||
|
risques,
|
||||||
|
cas: selectedCase.key || '',
|
||||||
|
formateur: stagiaire.formateur || '',
|
||||||
|
nom: stagiaire.nom || '',
|
||||||
|
date: stagiaire.date || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let minuteurSauvegarde = null;
|
||||||
|
function sauvegarder() {
|
||||||
|
clearTimeout(minuteurSauvegarde);
|
||||||
|
minuteurSauvegarde = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CLE_SAUVEGARDE, JSON.stringify(etatCourant()));
|
||||||
|
} catch (e) { /* navigation privée ou quota : on continue sans sauvegarde */ }
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lireSauvegarde() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(CLE_SAUVEGARDE)); }
|
||||||
|
catch (e) { return null; }
|
||||||
|
}
|
||||||
|
function effacerSauvegarde() {
|
||||||
|
try { localStorage.removeItem(CLE_SAUVEGARDE); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appliquerSauvegarde(data) {
|
||||||
|
Object.entries(data.champs || {}).forEach(([id, v]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = v;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recrée autant de lignes de risques que nécessaire avant de les remplir
|
||||||
|
const risques = data.risques || [];
|
||||||
|
const tbody = document.getElementById('riskTableBody');
|
||||||
|
while (tbody.querySelectorAll('.risk-row').length < risques.length) addRiskRow();
|
||||||
|
tbody.querySelectorAll('.risk-row').forEach((row, i) => {
|
||||||
|
const valeurs = risques[i] || [];
|
||||||
|
row.querySelectorAll('.risk-cell').forEach((cell, j) => { cell.value = valeurs[j] || ''; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Le cas pratique et le formateur passent par leurs boutons, qui remettent
|
||||||
|
// aussi l'interface (carte sélectionnée, bouton de confirmation) dans le bon état.
|
||||||
|
if (data.cas) {
|
||||||
|
const carte = Array.from(document.querySelectorAll('.case-card'))
|
||||||
|
.find(c => (c.getAttribute('onclick') || '').includes("'" + data.cas + "'"));
|
||||||
|
if (carte) carte.click();
|
||||||
|
}
|
||||||
|
if (data.formateur) {
|
||||||
|
const btn = document.getElementById('btn' + data.formateur);
|
||||||
|
if (btn) btn.click();
|
||||||
|
}
|
||||||
|
stagiaire.nom = data.nom || stagiaire.nom;
|
||||||
|
stagiaire.date = data.date || stagiaire.date;
|
||||||
|
|
||||||
|
reponsesModifiees = true;
|
||||||
|
if (typeof data.slide === 'number' && data.slide > 0) goTo(data.slide);
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bandeau de restauration : on ne restaure jamais sans demander, pour qu'un
|
||||||
|
nouveau groupe n'hérite pas des réponses du précédent sur un poste partagé. */
|
||||||
|
function injecterStylesBandeau() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.restore-bar {
|
||||||
|
position: fixed; top: 84px; left: 50%; transform: translateX(-50%);
|
||||||
|
z-index: 900; max-width: min(680px, calc(100vw - 32px));
|
||||||
|
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
||||||
|
background: var(--white); border: 1.5px solid var(--amber);
|
||||||
|
border-radius: 14px; padding: 14px 18px;
|
||||||
|
box-shadow: 0 8px 28px rgba(0,0,0,0.12); font-size: 13.5px;
|
||||||
|
}
|
||||||
|
.restore-bar .restore-text { flex: 1; min-width: 220px; line-height: 1.5; }
|
||||||
|
.restore-bar .restore-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||||
|
.restore-bar button {
|
||||||
|
font-family: 'Syne', sans-serif; font-weight: 700; font-size: 12.5px;
|
||||||
|
padding: 8px 16px; border-radius: 20px; cursor: pointer;
|
||||||
|
border: 1px solid var(--border); background: transparent; color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.restore-bar button:hover { border-color: var(--amber); color: var(--amber); }
|
||||||
|
.restore-bar button.primary {
|
||||||
|
background: var(--amber); border-color: var(--amber); color: #fff;
|
||||||
|
}
|
||||||
|
.restore-bar button.primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: #fff; }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposerRestauration() {
|
||||||
|
const data = lireSauvegarde();
|
||||||
|
if (!data) return;
|
||||||
|
const aDesReponses = (data.champs && Object.keys(data.champs).length > 0)
|
||||||
|
|| (data.risques || []).some(r => r.some(v => v));
|
||||||
|
if (!aDesReponses) return;
|
||||||
|
|
||||||
|
const quand = new Date(data.savedAt);
|
||||||
|
const libelle = isNaN(quand.getTime()) ? '' :
|
||||||
|
` du ${quand.toLocaleDateString('fr-FR')} à ${quand.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
||||||
|
|
||||||
|
injecterStylesBandeau();
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'restore-bar';
|
||||||
|
bar.innerHTML = `
|
||||||
|
<span class="restore-text">💾 Des réponses enregistrées${libelle} ont été retrouvées sur ce poste.</span>
|
||||||
|
<span class="restore-actions">
|
||||||
|
<button type="button" class="primary" id="btnRestaurer">Restaurer</button>
|
||||||
|
<button type="button" id="btnRepartir">Repartir de zéro</button>
|
||||||
|
</span>`;
|
||||||
|
document.body.appendChild(bar);
|
||||||
|
|
||||||
|
document.getElementById('btnRestaurer').onclick = () => { appliquerSauvegarde(data); bar.remove(); };
|
||||||
|
document.getElementById('btnRepartir').onclick = () => { effacerSauvegarde(); bar.remove(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
proposerRestauration();
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,650 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Correction TP Nuisibles — Certibiocide</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="../header-style.css"/>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding: calc(36px + 70px) 20px 80px;
|
|
||||||
}
|
|
||||||
.wrap { max-width: 820px; margin: 0 auto; }
|
|
||||||
.syne { font-family: 'Syne', sans-serif; }
|
|
||||||
.eyebrow {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--amber);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DROPZONE */
|
|
||||||
#dropzone {
|
|
||||||
background: #fff;
|
|
||||||
border: 2px dashed rgba(186,117,23,0.35);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 56px 24px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.2s, background 0.2s;
|
|
||||||
}
|
|
||||||
#dropzone.drag { border-color: var(--amber); background: var(--amber-light); }
|
|
||||||
#dropzone .icon { font-size: 40px; margin-bottom: 12px; }
|
|
||||||
#dropzone p.title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 16px; margin-bottom: 6px; }
|
|
||||||
#dropzone p.sub { color: var(--text-muted); font-size: 13.5px; margin-bottom: 20px; }
|
|
||||||
#filePicker { display: none; }
|
|
||||||
.btn-pick {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--amber);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 10px 26px;
|
|
||||||
border-radius: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
#status { margin-top: 16px; font-size: 13px; }
|
|
||||||
#status.loading { color: var(--amber); }
|
|
||||||
#status.error { color: var(--coral); }
|
|
||||||
|
|
||||||
/* RESULT */
|
|
||||||
#result { display: none; }
|
|
||||||
.toprow {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
#fileName { font-size: 12.5px; color: var(--text-muted); }
|
|
||||||
.btn-reset {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.btn-reset:hover { border-color: var(--amber); color: var(--amber); }
|
|
||||||
|
|
||||||
.id-card { display: none; }
|
|
||||||
|
|
||||||
/* BANDEAU PARTICIPANTS */
|
|
||||||
#participantsBanner {
|
|
||||||
background: linear-gradient(135deg, var(--amber) 0%, #D8923A 100%);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 20px 26px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#participantsBanner .pb-icon {
|
|
||||||
width: 48px; height: 48px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
#participantsBanner .pb-group {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0.85;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
#participantsBanner .pb-names {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.id-card .label {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 10.5px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--amber-dark);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.id-card .value { font-size: 14.5px; margin-top: 2px; }
|
|
||||||
|
|
||||||
.progress-row { display: flex; align-items: center; gap: 10px; margin-bottom: 30px; font-size: 13px; color: var(--text-muted); }
|
|
||||||
.progress-track { flex: 1; height: 7px; background: rgba(0,0,0,0.08); border-radius: 6px; overflow: hidden; }
|
|
||||||
.progress-fill { height: 100%; background: var(--teal); transition: width 0.4s ease; }
|
|
||||||
.progress-row strong { font-family: 'Syne', sans-serif; color: var(--text); }
|
|
||||||
|
|
||||||
/* CARROUSEL DE THEMES */
|
|
||||||
.carousel-wrap { margin-bottom: 36px; }
|
|
||||||
.carousel-nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.nav-btn {
|
|
||||||
width: 38px; height: 38px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
transition: background 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
.nav-btn:hover { background: var(--amber-light); border-color: var(--amber); }
|
|
||||||
.nav-btn:disabled { opacity: 0.3; cursor: default; }
|
|
||||||
.nav-btn:disabled:hover { background: #fff; border-color: var(--border); }
|
|
||||||
.dots { display: flex; gap: 7px; }
|
|
||||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(0,0,0,0.15); cursor: pointer; transition: background 0.15s, transform 0.15s; }
|
|
||||||
.dot.active { background: var(--amber); transform: scale(1.3); }
|
|
||||||
|
|
||||||
.theme-panel {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 26px 28px;
|
|
||||||
min-height: 420px;
|
|
||||||
}
|
|
||||||
.theme-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
.theme-icon {
|
|
||||||
width: 44px; height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 22px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.theme-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 17px; }
|
|
||||||
.theme-count { font-size: 12.5px; color: var(--text-muted); margin-left: auto; }
|
|
||||||
|
|
||||||
.cards { display: flex; flex-direction: column; gap: 10px; }
|
|
||||||
.qcard {
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
transition: box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.qcard:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.06); }
|
|
||||||
.qcard .badge {
|
|
||||||
width: 22px; height: 22px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
.qcard .badge.ok { background: var(--teal-light); color: var(--teal); }
|
|
||||||
.qcard .badge.miss { background: var(--coral-light); color: var(--coral); }
|
|
||||||
.qcard .qtext { flex: 1; }
|
|
||||||
.qcard .qlabel { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
|
|
||||||
.qcard .qanswer { font-size: 13.5px; white-space: pre-wrap; }
|
|
||||||
.qcard .qanswer.empty { color: var(--coral); font-style: italic; }
|
|
||||||
|
|
||||||
.table-wrap {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TABLEAU ANALYSE DE RISQUES */
|
|
||||||
.risk-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
||||||
.risk-table th {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 11.5px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: var(--amber-dark);
|
|
||||||
background: var(--amber-light);
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
.risk-table th:first-child { border-radius: 8px 0 0 8px; }
|
|
||||||
.risk-table th:last-child { border-radius: 0 8px 8px 0; }
|
|
||||||
.risk-table td {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
vertical-align: top;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.risk-table tr:last-child td { border-bottom: none; }
|
|
||||||
.meta-line { font-size: 12.5px; color: var(--text-muted); margin-top: 20px; }
|
|
||||||
.meta-line strong { color: var(--text); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div class="logo">NextGN Formation</div>
|
|
||||||
<nav>
|
|
||||||
<a href="../index.html">Accueil</a>
|
|
||||||
<a href="../index.html#formations">Exercices</a>
|
|
||||||
<a href="../index.html#documentation">Documentation</a>
|
|
||||||
<a href="../index.html#rapports">Rapports</a>
|
|
||||||
<a href="../index.html#about">À propos</a>
|
|
||||||
<a href="../index.html#contact">Contact</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="wrap">
|
|
||||||
<div id="participantsBanner" style="display:none;"></div>
|
|
||||||
<div class="eyebrow">Certibiocide — Correction de TP</div>
|
|
||||||
<h1>TP Nuisibles — Lecture du rendu</h1>
|
|
||||||
|
|
||||||
<div id="dropzone">
|
|
||||||
<div class="icon">📄</div>
|
|
||||||
<p class="title">Déposez le PDF du stagiaire ici</p>
|
|
||||||
<p class="sub">ou cliquez pour sélectionner le fichier exporté depuis le TP</p>
|
|
||||||
<label class="btn-pick">Choisir un fichier
|
|
||||||
<input type="file" id="filePicker" accept="application/pdf">
|
|
||||||
</label>
|
|
||||||
<div id="status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="result">
|
|
||||||
<div class="toprow">
|
|
||||||
<span id="fileName"></span>
|
|
||||||
<button class="btn-reset" onclick="resetAll()">↺ Charger un autre PDF</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="id-card" id="idCard"></div>
|
|
||||||
|
|
||||||
<div class="progress-row">
|
|
||||||
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
|
|
||||||
<span><strong id="progressCount"></strong> questions répondues</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="carousel-wrap">
|
|
||||||
<div class="carousel-nav">
|
|
||||||
<button class="nav-btn" id="prevBtn" onclick="goTo(currentTheme - 1)">←</button>
|
|
||||||
<div class="dots" id="dots"></div>
|
|
||||||
<button class="nav-btn" id="nextBtn" onclick="goTo(currentTheme + 1)">→</button>
|
|
||||||
</div>
|
|
||||||
<div id="themePanel"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="syne" style="font-size:16px;font-weight:800;margin-bottom:12px;color:var(--amber-dark);">Analyse de risques</h2>
|
|
||||||
<div class="table-wrap" id="tp3Meta"></div>
|
|
||||||
<div class="table-wrap" id="tp3Table" style="display:none;"></div>
|
|
||||||
<p class="meta-line" id="suiviLine"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2026 NextGN Formation - Formations Certibiocide & Techniques</p>
|
|
||||||
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
|
||||||
<script>
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
|
|
||||||
|
|
||||||
// Thèmes regroupant les sections du TP, avec icône et couleur
|
|
||||||
const THEMES = [
|
|
||||||
{
|
|
||||||
title: "Premier contact & diagnostic",
|
|
||||||
icon: "🗣️", color: "purple",
|
|
||||||
items: [
|
|
||||||
"Questions posées au client lors du premier contact",
|
|
||||||
"Conseils donnés en premier lieu",
|
|
||||||
"Actions de prévention menées sur le site",
|
|
||||||
"Éléments observés / preuve de présence du nuisible",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Solutions envisagées",
|
|
||||||
icon: "🧰", color: "blue",
|
|
||||||
items: [
|
|
||||||
"Solutions alternatives non-biocides",
|
|
||||||
"Solutions biocides mises en place",
|
|
||||||
"Type de produit (TP) et mode de traitement",
|
|
||||||
"Suivi mis en place et durée",
|
|
||||||
"Gestion des déchets",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Produit & traçabilité",
|
|
||||||
icon: "🧪", color: "teal",
|
|
||||||
items: [
|
|
||||||
"FDS trouvée (éléments principaux)",
|
|
||||||
"Nom du produit et fabricant/fournisseur",
|
|
||||||
"Numéro AMM",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Sécurité & santé",
|
|
||||||
icon: "🛡️", color: "coral",
|
|
||||||
items: [
|
|
||||||
"Risques pour la santé",
|
|
||||||
"EPI à porter",
|
|
||||||
"Risques environnement / dispersion accidentelle",
|
|
||||||
"DL50 substance(s) active(s)",
|
|
||||||
"Gestes de premiers secours",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ALL_SECTIONS = THEMES.flatMap(t => t.items);
|
|
||||||
|
|
||||||
const COLOR_MAP = {
|
|
||||||
purple: { bg: "var(--purple-light)", fg: "var(--purple)" },
|
|
||||||
blue: { bg: "var(--blue-light)", fg: "var(--blue)" },
|
|
||||||
teal: { bg: "var(--teal-light)", fg: "var(--teal)" },
|
|
||||||
coral: { bg: "var(--coral-light)", fg: "var(--coral)" },
|
|
||||||
};
|
|
||||||
|
|
||||||
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
||||||
function escapeHtml(s) {
|
|
||||||
if (!s) return "";
|
|
||||||
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
||||||
}
|
|
||||||
function isEmpty(v) {
|
|
||||||
if (!v) return true;
|
|
||||||
const t = v.trim();
|
|
||||||
return t === "" || /^\(sans réponse\)$/i.test(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function captureBetween(text, label, nextLabels) {
|
|
||||||
const startRx = new RegExp(escapeRx(label) + "\\s*:?\\s*", "i");
|
|
||||||
const m = startRx.exec(text);
|
|
||||||
if (!m) return "";
|
|
||||||
const from = m.index + m[0].length;
|
|
||||||
let to = text.length;
|
|
||||||
for (const nl of nextLabels) {
|
|
||||||
const endRx = new RegExp(escapeRx(nl), "i");
|
|
||||||
const m2 = endRx.exec(text.slice(from));
|
|
||||||
if (m2 && from + m2.index < to) to = from + m2.index;
|
|
||||||
}
|
|
||||||
return text.slice(from, to).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePdfText(raw) {
|
|
||||||
let clean = raw.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
||||||
// Retire les répétitions d'en-tête de page (ex: "17/06/2026 15:42 Résultats TP — ... about:blank 1/2")
|
|
||||||
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2}\s+Résultats TP[^]*?about:blank\s*\d\/\d/gi, " ");
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
nom: captureBetween(clean, "Groupe / Stagiaire", ["Date de session"]),
|
|
||||||
date_session: captureBetween(clean, "Date de session", ["Formateur"]),
|
|
||||||
formateur: captureBetween(clean, "Formateur", ["Cas pratique"]),
|
|
||||||
cas: captureBetween(clean, "Cas pratique", ["Réponses TP1"]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sépare "Groupe N" du nom des participants (ex: "Groupe 1 Romain Kevin Valere Greg P")
|
|
||||||
const groupMatch = /^Groupe\s*(\d+)\s*(.*)$/i.exec(meta.nom.trim());
|
|
||||||
if (groupMatch) {
|
|
||||||
meta.groupe = "Groupe " + groupMatch[1];
|
|
||||||
meta.participants = groupMatch[2].trim();
|
|
||||||
} else {
|
|
||||||
meta.groupe = "";
|
|
||||||
meta.participants = meta.nom.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const startIdx = clean.search(/Réponses TP1\s*&?\s*TP2/i);
|
|
||||||
const endIdx = clean.search(/TP3\s*[—-]\s*Analyse de risques/i);
|
|
||||||
const block = startIdx >= 0 ? clean.slice(startIdx, endIdx >= 0 ? endIdx : clean.length) : clean;
|
|
||||||
|
|
||||||
const answers = {};
|
|
||||||
for (let i = 0; i < ALL_SECTIONS.length; i++) {
|
|
||||||
const label = ALL_SECTIONS[i];
|
|
||||||
const nextLabel = ALL_SECTIONS[i + 1];
|
|
||||||
answers[label] = captureBetween(block, label, nextLabel ? [nextLabel] : []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tp3Zone = endIdx >= 0 ? clean.slice(endIdx) : "";
|
|
||||||
const tp3 = {
|
|
||||||
intitule: captureBetween(tp3Zone, "Intitulé", ["Description"]),
|
|
||||||
description: captureBetween(tp3Zone, "Description", ["Lieu"]),
|
|
||||||
lieu: captureBetween(tp3Zone, "Lieu", ["Début prévu le"]),
|
|
||||||
debut: captureBetween(tp3Zone, "Début prévu le", ["Étapes"]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tableau de risques : on découpe en s'appuyant sur les intitulés d'étapes connus du TP
|
|
||||||
const KNOWN_STEPS = [
|
|
||||||
"remplir la poudreuse",
|
|
||||||
"sécurisation du site",
|
|
||||||
"application traitements",
|
|
||||||
"rangement du matériel. purger la perche",
|
|
||||||
"retrait du nid",
|
|
||||||
"gestion des déchets. élimination par des filiaires spé.",
|
|
||||||
];
|
|
||||||
let riskRows = [];
|
|
||||||
const tableMatch = /Étapes\s*Risques\s*Mesures de prévention([\s\S]*?)(Réalisée par|$)/i.exec(tp3Zone);
|
|
||||||
if (tableMatch) {
|
|
||||||
let raw = tableMatch[1].trim();
|
|
||||||
if (raw) {
|
|
||||||
// Repère les positions de chaque étape connue dans le texte aplati
|
|
||||||
const positions = [];
|
|
||||||
KNOWN_STEPS.forEach(step => {
|
|
||||||
const idx = raw.toLowerCase().indexOf(step.toLowerCase());
|
|
||||||
if (idx >= 0) positions.push({ step, idx });
|
|
||||||
});
|
|
||||||
positions.sort((a, b) => a.idx - b.idx);
|
|
||||||
|
|
||||||
if (positions.length > 0) {
|
|
||||||
for (let i = 0; i < positions.length; i++) {
|
|
||||||
const from = positions[i].idx + positions[i].step.length;
|
|
||||||
const to = i + 1 < positions.length ? positions[i + 1].idx : raw.length;
|
|
||||||
let rest = raw.slice(from, to).trim();
|
|
||||||
// rest contient "risque... mesure..." -> on coupe sur les mots clés de mesures (gants/masques, barrieres, etc.)
|
|
||||||
const measureRx = /(gants\s*\/\s*masques\s*\/\s*lunettes[^.]*|barrieres\.?\s*epi)/i;
|
|
||||||
const mm = measureRx.exec(rest);
|
|
||||||
let risque = rest, mesure = "";
|
|
||||||
if (mm) {
|
|
||||||
risque = rest.slice(0, mm.index).trim();
|
|
||||||
mesure = rest.slice(mm.index).trim();
|
|
||||||
}
|
|
||||||
risque = risque.replace(/^[\s\-–.]+/, "").trim();
|
|
||||||
riskRows.push({ etape: positions[i].step, risque: risque || "—", mesure: mesure || "—" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
riskRows.push({ etape: raw, risque: "", mesure: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mReal = clean.match(/Réalisée par\s*:\s*([^—\n]*)/i);
|
|
||||||
const mVerif = clean.match(/Vérifiée par\s*:\s*([^—\n]*)/i);
|
|
||||||
const mMaj = clean.match(/Mise à jour par\s*:\s*([^—\n]*)/i);
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta, answers, tp3, riskRows,
|
|
||||||
suivi: {
|
|
||||||
realise: mReal ? mReal[1].trim() : "",
|
|
||||||
verifie: mVerif ? mVerif[1].trim() : "",
|
|
||||||
maj: mMaj ? mMaj[1].trim() : "",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentThemeData = null;
|
|
||||||
let currentTheme = 0;
|
|
||||||
|
|
||||||
function renderThemePanel() {
|
|
||||||
const theme = THEMES[currentTheme];
|
|
||||||
const data = currentThemeData;
|
|
||||||
const colors = COLOR_MAP[theme.color];
|
|
||||||
const themeAnswered = theme.items.filter(s => !isEmpty(data.answers[s])).length;
|
|
||||||
|
|
||||||
document.getElementById("themePanel").innerHTML = `
|
|
||||||
<div class="theme-panel">
|
|
||||||
<div class="theme-head">
|
|
||||||
<div class="theme-icon" style="background:${colors.bg};color:${colors.fg};">${theme.icon}</div>
|
|
||||||
<div class="theme-title">${theme.title}</div>
|
|
||||||
<div class="theme-count">${themeAnswered}/${theme.items.length}</div>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
${theme.items.map(label => {
|
|
||||||
const value = data.answers[label] || "";
|
|
||||||
const empty = isEmpty(value);
|
|
||||||
return `
|
|
||||||
<div class="qcard">
|
|
||||||
<div class="badge ${empty ? 'miss' : 'ok'}">${empty ? '!' : '✓'}</div>
|
|
||||||
<div class="qtext">
|
|
||||||
<div class="qlabel">${escapeHtml(label)}</div>
|
|
||||||
<div class="qanswer ${empty ? 'empty' : ''}">${empty ? 'Sans réponse' : escapeHtml(value)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById("prevBtn").disabled = currentTheme === 0;
|
|
||||||
document.getElementById("nextBtn").disabled = currentTheme === THEMES.length - 1;
|
|
||||||
document.querySelectorAll(".dot").forEach((d, i) => d.classList.toggle("active", i === currentTheme));
|
|
||||||
}
|
|
||||||
|
|
||||||
function goTo(idx) {
|
|
||||||
if (idx < 0 || idx >= THEMES.length) return;
|
|
||||||
currentTheme = idx;
|
|
||||||
renderThemePanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResult(data) {
|
|
||||||
currentThemeData = data;
|
|
||||||
currentTheme = 0;
|
|
||||||
|
|
||||||
const banner = document.getElementById("participantsBanner");
|
|
||||||
if (data.meta.participants) {
|
|
||||||
banner.style.display = "flex";
|
|
||||||
banner.innerHTML = `
|
|
||||||
<div class="pb-icon">👥</div>
|
|
||||||
<div>
|
|
||||||
<div class="pb-group">${escapeHtml(data.meta.groupe) || "Participants"}</div>
|
|
||||||
<div class="pb-names">${escapeHtml(data.meta.participants)}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
banner.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("idCard").innerHTML = `
|
|
||||||
<div><div class="label">Groupe / Stagiaire</div><div class="value">${escapeHtml(data.meta.nom) || "—"}</div></div>
|
|
||||||
<div><div class="label">Date de session</div><div class="value">${escapeHtml(data.meta.date_session) || "—"}</div></div>
|
|
||||||
<div><div class="label">Formateur</div><div class="value">${escapeHtml(data.meta.formateur) || "—"}</div></div>
|
|
||||||
<div><div class="label">Cas pratique</div><div class="value">${escapeHtml(data.meta.cas) || "—"}</div></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const answeredCount = ALL_SECTIONS.filter(s => !isEmpty(data.answers[s])).length;
|
|
||||||
document.getElementById("progressFill").style.width = (answeredCount / ALL_SECTIONS.length * 100) + "%";
|
|
||||||
document.getElementById("progressCount").textContent = `${answeredCount} / ${ALL_SECTIONS.length}`;
|
|
||||||
|
|
||||||
document.getElementById("dots").innerHTML = THEMES.map((_, i) =>
|
|
||||||
`<div class="dot${i === 0 ? ' active' : ''}" onclick="goTo(${i})"></div>`
|
|
||||||
).join("");
|
|
||||||
|
|
||||||
renderThemePanel();
|
|
||||||
|
|
||||||
document.getElementById("tp3Meta").innerHTML = `
|
|
||||||
<p style="margin-bottom:6px;"><strong>Intitulé :</strong> ${escapeHtml(data.tp3.intitule) || '<em style="color:var(--coral)">—</em>'}</p>
|
|
||||||
<p style="margin-bottom:6px;"><strong>Description :</strong> ${escapeHtml(data.tp3.description) || '<em style="color:var(--coral)">—</em>'}</p>
|
|
||||||
<p style="margin-bottom:6px;"><strong>Lieu :</strong> ${escapeHtml(data.tp3.lieu) || '<em style="color:var(--coral)">—</em>'}</p>
|
|
||||||
<p><strong>Début prévu le :</strong> ${escapeHtml(data.tp3.debut) || '<em style="color:var(--coral)">—</em>'}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const tp3TableEl = document.getElementById("tp3Table");
|
|
||||||
if (data.riskRows && data.riskRows.length > 0) {
|
|
||||||
tp3TableEl.style.display = "block";
|
|
||||||
tp3TableEl.innerHTML = `
|
|
||||||
<table class="risk-table">
|
|
||||||
<thead><tr><th>Étapes</th><th>Risques</th><th>Mesures de prévention</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
${data.riskRows.map(r => `
|
|
||||||
<tr>
|
|
||||||
<td>${escapeHtml(r.etape) || "—"}</td>
|
|
||||||
<td>${escapeHtml(r.risque) || "—"}</td>
|
|
||||||
<td>${escapeHtml(r.mesure) || "—"}</td>
|
|
||||||
</tr>
|
|
||||||
`).join("")}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
tp3TableEl.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("suiviLine").innerHTML =
|
|
||||||
`Réalisée par : <strong>${escapeHtml(data.suivi.realise) || "—"}</strong> — Vérifiée par : <strong>${escapeHtml(data.suivi.verifie) || "—"}</strong> — Mise à jour par : <strong>${escapeHtml(data.suivi.maj) || "—"}</strong>`;
|
|
||||||
|
|
||||||
document.getElementById("dropzone").style.display = "none";
|
|
||||||
document.getElementById("result").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFile(file) {
|
|
||||||
if (!file) return;
|
|
||||||
const statusEl = document.getElementById("status");
|
|
||||||
statusEl.className = "loading";
|
|
||||||
statusEl.textContent = "Lecture du PDF en cours…";
|
|
||||||
document.getElementById("fileName").textContent = file.name;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
||||||
let fullText = "";
|
|
||||||
for (let p = 1; p <= pdf.numPages; p++) {
|
|
||||||
const page = await pdf.getPage(p);
|
|
||||||
const content = await page.getTextContent();
|
|
||||||
fullText += content.items.map(it => it.str).join(" ") + "\n";
|
|
||||||
}
|
|
||||||
const data = parsePdfText(fullText);
|
|
||||||
renderResult(data);
|
|
||||||
statusEl.textContent = "";
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
statusEl.className = "error";
|
|
||||||
statusEl.textContent = "Impossible de lire ce PDF. Vérifiez qu'il s'agit bien d'un export du TP.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAll() {
|
|
||||||
document.getElementById("result").style.display = "none";
|
|
||||||
document.getElementById("dropzone").style.display = "block";
|
|
||||||
document.getElementById("participantsBanner").style.display = "none";
|
|
||||||
document.getElementById("status").textContent = "";
|
|
||||||
document.getElementById("filePicker").value = "";
|
|
||||||
document.getElementById("tp3Table").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropzone = document.getElementById("dropzone");
|
|
||||||
dropzone.addEventListener("dragover", e => { e.preventDefault(); dropzone.classList.add("drag"); });
|
|
||||||
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag"));
|
|
||||||
dropzone.addEventListener("drop", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropzone.classList.remove("drag");
|
|
||||||
const file = e.dataTransfer.files?.[0];
|
|
||||||
if (file) handleFile(file);
|
|
||||||
});
|
|
||||||
document.getElementById("filePicker").addEventListener("change", e => handleFile(e.target.files?.[0]));
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1204
exercices/corriger-exercice.html
Normal file
1204
exercices/corriger-exercice.html
Normal file
File diff suppressed because it is too large
Load Diff
55
exercices/memory-pictogrammes/api/README.md
Normal file
55
exercices/memory-pictogrammes/api/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# API du classement Memory (ASP.NET + SQL Server)
|
||||||
|
|
||||||
|
Petit service **ASP.NET (C#) + SQL Server** qui héberge en ligne le
|
||||||
|
**classement partagé** du jeu Memory. Tous les joueurs enregistrent leurs
|
||||||
|
scores au même endroit et voient le même top 10 par mode (facile / normal /
|
||||||
|
difficile).
|
||||||
|
|
||||||
|
> Adapté à l'hébergement **FreeASPHosting.net / bsite.net** (Windows, IIS,
|
||||||
|
> base MSSQL 2016). Le fichier `scores.ashx` est compilé au vol par IIS :
|
||||||
|
> **aucune étape de build**, il suffit de le déposer.
|
||||||
|
|
||||||
|
## Contenu
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `scores.ashx` | Point d'entrée de l'API (GET pour lire, POST pour enregistrer). Crée la table automatiquement au premier appel. |
|
||||||
|
| `web.config.example` | Modèle de configuration à copier en `web.config`. |
|
||||||
|
| `web.config` | **À créer sur le serveur** avec le mot de passe SQL Server (clé `MemoryDb`). Non versionné. |
|
||||||
|
| `README.md` | Ce fichier. |
|
||||||
|
|
||||||
|
## Installation sur bsite.net
|
||||||
|
|
||||||
|
1. **Récupérer le mot de passe** de la base dans le panneau
|
||||||
|
*Manage Databases*. La base existe déjà :
|
||||||
|
- Serveur : `sql.bsite.net\MSSQL2016`
|
||||||
|
- Base / utilisateur : `nextgnformation_reponses_exercices`
|
||||||
|
|
||||||
|
2. **Créer `web.config`** à partir du modèle (`cp web.config.example web.config`),
|
||||||
|
puis remplacer `VOTRE_MOT_DE_PASSE` par le mot de passe réel dans la clé
|
||||||
|
`MemoryDb`.
|
||||||
|
|
||||||
|
3. **Déposer le dossier `api/`** via *My Files*, au même endroit que
|
||||||
|
`memory.html` (chemin attendu par le jeu :
|
||||||
|
`exercices/memory-pictogrammes/api/scores.ashx`).
|
||||||
|
|
||||||
|
4. C'est prêt : au premier score enregistré (ou premier appel GET), la table
|
||||||
|
`memory_scores` est créée automatiquement.
|
||||||
|
|
||||||
|
## Vérification rapide
|
||||||
|
|
||||||
|
- Lecture du classement :
|
||||||
|
`GET https://nextgnformation.bsite.net/exercices/memory-pictogrammes/api/scores.ashx?difficulty=all`
|
||||||
|
doit renvoyer `{"ok":true,"scores":[...]}`.
|
||||||
|
|
||||||
|
- Seuls les 10 meilleurs de chaque mode sont conservés : après chaque
|
||||||
|
enregistrement, les scores hors top 10 du mode concerné sont supprimés.
|
||||||
|
|
||||||
|
## Points d'attention
|
||||||
|
|
||||||
|
- **Ne poussez jamais `web.config` avec le vrai mot de passe dans un dépôt
|
||||||
|
public.** Éditez-le directement sur le serveur via *My Files*.
|
||||||
|
- Les statistiques personnelles (« Vos statistiques ») restent stockées
|
||||||
|
localement dans le navigateur de chaque joueur ; seul le classement est en ligne.
|
||||||
|
- Le score étant calculé côté navigateur, l'API valide les types et les bornes
|
||||||
|
mais ne garantit pas l'absence de triche (recalcul serveur possible plus tard).
|
||||||
329
exercices/memory-pictogrammes/api/scores.ashx
Normal file
329
exercices/memory-pictogrammes/api/scores.ashx
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<%@ WebHandler Language="C#" Class="ScoresHandler" %>
|
||||||
|
/*
|
||||||
|
* API du classement Memory (pictogrammes de sécurité) — ASP.NET + SQL Server.
|
||||||
|
*
|
||||||
|
* GET scores.ashx?difficulty=easy|normal|hard|all[&limit=10]
|
||||||
|
* -> renvoie le classement (top N) trié par score décroissant.
|
||||||
|
*
|
||||||
|
* POST scores.ashx (corps JSON)
|
||||||
|
* { playerName, userId, score, moves, timeSeconds, difficulty, isPerfect }
|
||||||
|
* -> enregistre le score, ne conserve que le top N par difficulté,
|
||||||
|
* renvoie { ok, saved, rank, isTop }.
|
||||||
|
*
|
||||||
|
* Seul le top N (défaut 10) de chaque mode est conservé : les scores plus
|
||||||
|
* faibles sont supprimés après chaque insertion.
|
||||||
|
*
|
||||||
|
* Fichier compilé au vol par IIS : il suffit de le déposer, aucune build.
|
||||||
|
* La chaîne de connexion est lue dans web.config (clé "MemoryDb").
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Configuration;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
public class ScoresHandler : IHttpHandler
|
||||||
|
{
|
||||||
|
static readonly string[] Difficulties = { "easy", "normal", "hard" };
|
||||||
|
const int TopN = 10;
|
||||||
|
|
||||||
|
public bool IsReusable { get { return false; } }
|
||||||
|
|
||||||
|
public void ProcessRequest(HttpContext ctx)
|
||||||
|
{
|
||||||
|
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
ctx.Response.AddHeader("X-Content-Type-Options", "nosniff");
|
||||||
|
|
||||||
|
var connString = WebConfigurationManager.ConnectionStrings["MemoryDb"] != null
|
||||||
|
? WebConfigurationManager.ConnectionStrings["MemoryDb"].ConnectionString
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connString))
|
||||||
|
{
|
||||||
|
Fail(ctx, 500, "Configuration manquante (connectionString 'MemoryDb' dans web.config).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var conn = new SqlConnection(connString))
|
||||||
|
{
|
||||||
|
conn.Open();
|
||||||
|
EnsureTable(conn);
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET")
|
||||||
|
{
|
||||||
|
HandleGet(ctx, conn);
|
||||||
|
}
|
||||||
|
else if (ctx.Request.HttpMethod == "POST")
|
||||||
|
{
|
||||||
|
HandlePost(ctx, conn);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 405;
|
||||||
|
ctx.Response.AddHeader("Allow", "GET, POST");
|
||||||
|
Write(ctx, new Dictionary<string, object> {
|
||||||
|
{ "ok", false }, { "error", "Méthode non autorisée." }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Fail(ctx, 500, "Erreur serveur.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Création de la table si besoin -------------------------------------
|
||||||
|
void EnsureTable(SqlConnection conn)
|
||||||
|
{
|
||||||
|
const string sql =
|
||||||
|
"IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'memory_scores') " +
|
||||||
|
"CREATE TABLE memory_scores (" +
|
||||||
|
" id INT IDENTITY(1,1) PRIMARY KEY," +
|
||||||
|
" player_name NVARCHAR(40) NOT NULL," +
|
||||||
|
" user_id NVARCHAR(64) NOT NULL," +
|
||||||
|
" score INT NOT NULL," +
|
||||||
|
" moves INT NOT NULL," +
|
||||||
|
" time_seconds INT NOT NULL," +
|
||||||
|
" difficulty NVARCHAR(16) NOT NULL," +
|
||||||
|
" is_perfect BIT NOT NULL DEFAULT 0," +
|
||||||
|
" created_at DATETIME NOT NULL" +
|
||||||
|
");";
|
||||||
|
using (var cmd = new SqlCommand(sql, conn)) { cmd.ExecuteNonQuery(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET : renvoie le classement ----------------------------------------
|
||||||
|
void HandleGet(HttpContext ctx, SqlConnection conn)
|
||||||
|
{
|
||||||
|
var difficulty = (ctx.Request.QueryString["difficulty"] ?? "all").Trim();
|
||||||
|
int limit;
|
||||||
|
if (!int.TryParse(ctx.Request.QueryString["limit"], out limit)) limit = TopN;
|
||||||
|
if (limit < 1) limit = 1;
|
||||||
|
if (limit > TopN) limit = TopN;
|
||||||
|
|
||||||
|
var rows = new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
|
if (difficulty == "all")
|
||||||
|
{
|
||||||
|
foreach (var diff in Difficulties)
|
||||||
|
rows.AddRange(FetchTop(conn, diff, TopN));
|
||||||
|
|
||||||
|
rows.Sort(CompareRows);
|
||||||
|
if (rows.Count > limit) rows = rows.GetRange(0, limit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Array.IndexOf(Difficulties, difficulty) < 0)
|
||||||
|
{
|
||||||
|
Fail(ctx, 400, "Difficulté inconnue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows = FetchTop(conn, difficulty, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(ctx, new Dictionary<string, object> { { "ok", true }, { "scores", rows } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- POST : enregistre un score puis élague au top N --------------------
|
||||||
|
void HandlePost(HttpContext ctx, SqlConnection conn)
|
||||||
|
{
|
||||||
|
string body;
|
||||||
|
using (var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||||||
|
body = reader.ReadToEnd();
|
||||||
|
|
||||||
|
Dictionary<string, object> data = null;
|
||||||
|
try { data = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(body); }
|
||||||
|
catch { data = null; }
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
Fail(ctx, 400, "Corps JSON invalide.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var difficulty = Str(data, "difficulty");
|
||||||
|
if (Array.IndexOf(Difficulties, difficulty) < 0)
|
||||||
|
{
|
||||||
|
Fail(ctx, 400, "Difficulté inconnue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyage / bornage des entrées
|
||||||
|
var playerName = Str(data, "playerName").Trim();
|
||||||
|
playerName = StripControlChars(playerName);
|
||||||
|
if (playerName.Length == 0) playerName = "Anonyme";
|
||||||
|
if (playerName.Length > 40) playerName = playerName.Substring(0, 40);
|
||||||
|
|
||||||
|
var userId = OnlyIdChars(Str(data, "userId"));
|
||||||
|
if (userId.Length > 64) userId = userId.Substring(0, 64);
|
||||||
|
if (userId.Length == 0) userId = "anon";
|
||||||
|
|
||||||
|
int score = Clamp(Int(data, "score"), 0, 100000000);
|
||||||
|
int moves = Clamp(Int(data, "moves"), 0, 100000);
|
||||||
|
int time = Clamp(Int(data, "timeSeconds"), 0, 1000000);
|
||||||
|
int isPerfect = Bool(data, "isPerfect") ? 1 : 0;
|
||||||
|
|
||||||
|
int insertedId;
|
||||||
|
using (var cmd = new SqlCommand(
|
||||||
|
"INSERT INTO memory_scores " +
|
||||||
|
"(player_name, user_id, score, moves, time_seconds, difficulty, is_perfect, created_at) " +
|
||||||
|
"OUTPUT INSERTED.id " +
|
||||||
|
"VALUES (@name, @uid, @score, @moves, @time, @diff, @perfect, GETUTCDATE())", conn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 40).Value = playerName;
|
||||||
|
cmd.Parameters.Add("@uid", SqlDbType.NVarChar, 64).Value = userId;
|
||||||
|
cmd.Parameters.Add("@score", SqlDbType.Int).Value = score;
|
||||||
|
cmd.Parameters.Add("@moves", SqlDbType.Int).Value = moves;
|
||||||
|
cmd.Parameters.Add("@time", SqlDbType.Int).Value = time;
|
||||||
|
cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty;
|
||||||
|
cmd.Parameters.Add("@perfect", SqlDbType.Bit).Value = isPerfect;
|
||||||
|
insertedId = (int)cmd.ExecuteScalar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ne conserver que le top N pour ce mode
|
||||||
|
using (var cmd = new SqlCommand(
|
||||||
|
"WITH ranked AS (" +
|
||||||
|
" SELECT id, ROW_NUMBER() OVER (" +
|
||||||
|
" PARTITION BY difficulty ORDER BY score DESC, time_seconds ASC, id ASC) AS rn" +
|
||||||
|
" FROM memory_scores WHERE difficulty = @diff" +
|
||||||
|
") DELETE FROM memory_scores WHERE id IN (SELECT id FROM ranked WHERE rn > @topN)", conn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty;
|
||||||
|
cmd.Parameters.Add("@topN", SqlDbType.Int).Value = TopN;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le score inséré a-t-il survécu à l'élagage ? Quel rang ?
|
||||||
|
var top = FetchTop(conn, difficulty, TopN);
|
||||||
|
int? rank = null;
|
||||||
|
for (int i = 0; i < top.Count; i++)
|
||||||
|
{
|
||||||
|
if ((int)top[i]["id"] == insertedId) { rank = i + 1; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(ctx, new Dictionary<string, object> {
|
||||||
|
{ "ok", true },
|
||||||
|
{ "saved", true },
|
||||||
|
{ "isTop", rank.HasValue },
|
||||||
|
{ "rank", rank.HasValue ? (object)rank.Value : null }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Accès données -------------------------------------------------------
|
||||||
|
List<Dictionary<string, object>> FetchTop(SqlConnection conn, string difficulty, int limit)
|
||||||
|
{
|
||||||
|
var result = new List<Dictionary<string, object>>();
|
||||||
|
using (var cmd = new SqlCommand(
|
||||||
|
"SELECT TOP (@limit) id, player_name, user_id, score, moves, time_seconds, " +
|
||||||
|
"difficulty, is_perfect, created_at " +
|
||||||
|
"FROM memory_scores WHERE difficulty = @diff " +
|
||||||
|
"ORDER BY score DESC, time_seconds ASC, id ASC", conn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.Add("@limit", SqlDbType.Int).Value = limit;
|
||||||
|
cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty;
|
||||||
|
using (var r = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (r.Read())
|
||||||
|
{
|
||||||
|
result.Add(new Dictionary<string, object> {
|
||||||
|
{ "id", (int)r["id"] },
|
||||||
|
{ "playerName", r["player_name"].ToString() },
|
||||||
|
{ "userId", r["user_id"].ToString() },
|
||||||
|
{ "score", (int)r["score"] },
|
||||||
|
{ "moves", (int)r["moves"] },
|
||||||
|
{ "timeSeconds", (int)r["time_seconds"] },
|
||||||
|
{ "difficulty", r["difficulty"].ToString() },
|
||||||
|
{ "isPerfect", Convert.ToBoolean(r["is_perfect"]) },
|
||||||
|
{ "date", ((DateTime)r["created_at"]).ToString("yyyy-MM-ddTHH:mm:ss") + "Z" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CompareRows(Dictionary<string, object> a, Dictionary<string, object> b)
|
||||||
|
{
|
||||||
|
int sa = (int)a["score"], sb = (int)b["score"];
|
||||||
|
if (sa != sb) return sb - sa;
|
||||||
|
return (int)a["timeSeconds"] - (int)b["timeSeconds"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sérialisation / helpers --------------------------------------------
|
||||||
|
void Write(HttpContext ctx, object payload)
|
||||||
|
{
|
||||||
|
// On retire la clé "id" (interne) des lignes avant l'envoi.
|
||||||
|
var scrubbed = Scrub(payload);
|
||||||
|
ctx.Response.Write(new JavaScriptSerializer().Serialize(scrubbed));
|
||||||
|
}
|
||||||
|
|
||||||
|
object Scrub(object payload)
|
||||||
|
{
|
||||||
|
var dict = payload as Dictionary<string, object>;
|
||||||
|
if (dict != null && dict.ContainsKey("scores"))
|
||||||
|
{
|
||||||
|
var list = dict["scores"] as List<Dictionary<string, object>>;
|
||||||
|
if (list != null)
|
||||||
|
foreach (var row in list) row.Remove("id");
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fail(HttpContext ctx, int status, string message)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = status;
|
||||||
|
ctx.Response.Write(new JavaScriptSerializer().Serialize(
|
||||||
|
new Dictionary<string, object> { { "ok", false }, { "error", message } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Str(Dictionary<string, object> d, string key)
|
||||||
|
{
|
||||||
|
object v; return (d.TryGetValue(key, out v) && v != null) ? v.ToString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Int(Dictionary<string, object> d, string key)
|
||||||
|
{
|
||||||
|
object v; int n;
|
||||||
|
if (d.TryGetValue(key, out v) && v != null && int.TryParse(v.ToString(), out n)) return n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Bool(Dictionary<string, object> d, string key)
|
||||||
|
{
|
||||||
|
object v;
|
||||||
|
if (!d.TryGetValue(key, out v) || v == null) return false;
|
||||||
|
if (v is bool) return (bool)v;
|
||||||
|
var s = v.ToString().ToLowerInvariant();
|
||||||
|
return s == "true" || s == "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Clamp(int value, int min, int max)
|
||||||
|
{
|
||||||
|
if (value < min) return min;
|
||||||
|
if (value > max) return max;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string StripControlChars(string s)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(s.Length);
|
||||||
|
foreach (var c in s) if (!char.IsControl(c)) sb.Append(c);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string OnlyIdChars(string s)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(s.Length);
|
||||||
|
foreach (var c in s)
|
||||||
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')
|
||||||
|
sb.Append(c);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
exercices/memory-pictogrammes/api/web.config.example
Normal file
16
exercices/memory-pictogrammes/api/web.config.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<!--
|
||||||
|
MODÈLE de configuration. Sur le serveur bsite.net :
|
||||||
|
1. Copiez ce fichier en « web.config ».
|
||||||
|
2. Renseignez le mot de passe fourni par le panneau « Manage Databases ».
|
||||||
|
|
||||||
|
Le vrai « web.config » n'est PAS versionné (voir .gitignore) afin de ne
|
||||||
|
jamais exposer le mot de passe dans l'historique Git.
|
||||||
|
-->
|
||||||
|
<connectionStrings>
|
||||||
|
<add name="MemoryDb"
|
||||||
|
connectionString="Server=sql.bsite.net\MSSQL2016;Database=nextgnformation_reponses_exercices;User Id=nextgnformation_reponses_exercices;Password=VOTRE_MOT_DE_PASSE;"
|
||||||
|
providerName="System.Data.SqlClient" />
|
||||||
|
</connectionStrings>
|
||||||
|
</configuration>
|
||||||
@@ -44,15 +44,15 @@
|
|||||||
@keyframes matchSuccess {
|
@keyframes matchSuccess {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
filter: drop-shadow(0 0 0px rgba(76, 175, 80, 0.6));
|
filter: drop-shadow(0 0 0px rgba(29, 158, 117, 0.6));
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: scale(1.15) rotate(5deg);
|
transform: scale(1.15) rotate(5deg);
|
||||||
filter: drop-shadow(0 0 12px rgba(76, 175, 80, 0.8));
|
filter: drop-shadow(0 0 12px rgba(29, 158, 117, 0.8));
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
filter: drop-shadow(0 0 0px rgba(76, 175, 80, 0));
|
filter: drop-shadow(0 0 0px rgba(29, 158, 117, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@
|
|||||||
/* Pulse Progress Bar */
|
/* Pulse Progress Bar */
|
||||||
@keyframes progressPulse {
|
@keyframes progressPulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
box-shadow: 0 0 8px rgba(29, 158, 117, 0.4);
|
box-shadow: 0 0 8px rgba(186, 117, 23, 0.4);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 24px rgba(29, 158, 117, 0.8);
|
box-shadow: 0 0 24px rgba(186, 117, 23, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar.pulse {
|
.game-progress-fill.pulse {
|
||||||
animation: progressPulse 0.6s ease-out;
|
animation: progressPulse 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
@keyframes statUpdate {
|
@keyframes statUpdate {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
background: rgba(29, 158, 117, 0.2);
|
background: rgba(186, 117, 23, 0.2);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
@@ -255,10 +255,10 @@
|
|||||||
/* Card glow hover */
|
/* Card glow hover */
|
||||||
@keyframes glowCard {
|
@keyframes glowCard {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0px rgba(29, 158, 117, 0);
|
box-shadow: 0 0 0px rgba(186, 117, 23, 0);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 16px rgba(29, 158, 117, 0.3);
|
box-shadow: 0 0 16px rgba(186, 117, 23, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,10 +281,10 @@
|
|||||||
/* Pulse button */
|
/* Pulse button */
|
||||||
@keyframes buttonPulse {
|
@keyframes buttonPulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
box-shadow: 0 0 0px rgba(29, 158, 117, 0.7);
|
box-shadow: 0 0 0px rgba(186, 117, 23, 0.7);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 16px rgba(29, 158, 117, 0.4);
|
box-shadow: 0 0 16px rgba(186, 117, 23, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
============================================ */
|
============================================ */
|
||||||
.game-hud {
|
.game-hud {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 32px;
|
flex: 1;
|
||||||
animation: fadeInUp 0.6s ease-out 0.4s both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-box {
|
.stat-box {
|
||||||
background: linear-gradient(135deg, var(--teal-light), var(--white));
|
background: linear-gradient(135deg, var(--amber-light), var(--white));
|
||||||
border: 2px solid var(--teal-mid);
|
border: 2px solid var(--amber-mid);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
.stat-box:hover {
|
.stat-box:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
border-color: var(--teal-dark);
|
border-color: var(--amber-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-box:hover::before {
|
.stat-box:hover::before {
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
.stat-box .value {
|
.stat-box .value {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
PROGRESS BAR (Paires trouvées)
|
PROGRESS BAR (Paires trouvées)
|
||||||
============================================ */
|
============================================ */
|
||||||
.progress-container {
|
.progress-container {
|
||||||
background: var(--teal-light);
|
background: var(--amber-light);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -82,14 +81,14 @@
|
|||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.game-progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(90deg, var(--amber-mid), var(--amber-dark));
|
||||||
width: 0%;
|
width: 0%;
|
||||||
transition: width 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: width 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container.pulse .progress-bar {
|
.progress-container.pulse .game-progress-fill {
|
||||||
animation: progressPulse 0.6s ease-out;
|
animation: progressPulse 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,15 +147,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: linear-gradient(135deg, var(--white), var(--teal-light));
|
background: linear-gradient(135deg, var(--white), var(--amber-light));
|
||||||
border-radius: 24px;
|
border-radius: 12px;
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
border: 2px solid var(--teal-mid);
|
border: 2px solid var(--amber-mid);
|
||||||
box-shadow: 0 32px 64px rgba(15, 110, 86, 0.3);
|
box-shadow: 0 32px 64px rgba(186, 117, 23, 0.3);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -177,7 +176,7 @@
|
|||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--teal);
|
color: var(--amber);
|
||||||
margin: 32px 0 12px;
|
margin: 32px 0 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -186,7 +185,7 @@
|
|||||||
.modal-content .score-display {
|
.modal-content .score-display {
|
||||||
font-size: 64px;
|
font-size: 64px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -203,10 +202,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stats-summary .stat {
|
.stats-summary .stat {
|
||||||
background: var(--teal-light);
|
background: var(--amber-light);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--teal-mid);
|
border: 1px solid var(--amber-mid);
|
||||||
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@
|
|||||||
|
|
||||||
.stats-summary .stat-value {
|
.stats-summary .stat-value {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--teal-dark);
|
color: var(--amber-dark);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
}
|
}
|
||||||
@@ -230,6 +229,41 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PLAYER NAME MODAL
|
||||||
|
============================================ */
|
||||||
|
.name-modal-content h2 {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-modal-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
border: 2px solid var(--amber-mid);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--text);
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--amber-dark);
|
||||||
|
box-shadow: 0 0 0 3px rgba(186, 117, 23, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
ACHIEVEMENTS
|
ACHIEVEMENTS
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -289,52 +323,6 @@
|
|||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
PAUSE MODAL
|
|
||||||
============================================ */
|
|
||||||
.pause-modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1999;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-content {
|
|
||||||
background: var(--white);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
border: 2px solid var(--teal-mid);
|
|
||||||
animation: slideUp 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-content h2 {
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 32px;
|
|
||||||
color: var(--teal);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-content .pause-timer {
|
|
||||||
font-size: 48px;
|
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--teal-dark);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
animation: timerTick 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
RESPONSIVE DESIGN
|
RESPONSIVE DESIGN
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -2,18 +2,25 @@
|
|||||||
LEADERBOARD & PLAYER STATS
|
LEADERBOARD & PLAYER STATS
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.leaderboard-section {
|
.leaderboard-panel {
|
||||||
margin-top: 80px;
|
animation: fadeInUp 0.6s ease-out 0.6s both;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
animation: fadeInUp 0.6s ease-out 0.6s both;
|
animation: fadeInUp 0.6s ease-out 0.6s both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-section h2 {
|
.leaderboard-panel h2 {
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: clamp(24px, 5vw, 32px);
|
font-size: clamp(20px, 4vw, 26px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--teal);
|
letter-spacing: -0.5px;
|
||||||
margin-bottom: 24px;
|
color: var(--text);
|
||||||
text-align: center;
|
margin: 0 auto 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 3px solid var(--amber-mid);
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -30,7 +37,7 @@
|
|||||||
.tab-btn {
|
.tab-btn {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border: 2px solid var(--teal-light);
|
border: 2px solid var(--amber-light);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -43,13 +50,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn:hover {
|
.tab-btn:hover {
|
||||||
border-color: var(--teal-mid);
|
border-color: var(--amber-mid);
|
||||||
color: var(--teal-mid);
|
color: var(--amber-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn.active {
|
.tab-btn.active {
|
||||||
background: linear-gradient(135deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
border-color: var(--teal-dark);
|
border-color: var(--amber-dark);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@@ -58,12 +65,13 @@
|
|||||||
LEADERBOARD LIST
|
LEADERBOARD LIST
|
||||||
============================================ */
|
============================================ */
|
||||||
.leaderboard {
|
.leaderboard {
|
||||||
background: linear-gradient(135deg, var(--teal-light), var(--white));
|
background: linear-gradient(135deg, var(--amber-light), var(--white));
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 2px solid var(--teal-mid);
|
border: 2px solid var(--amber-mid);
|
||||||
margin-bottom: 48px;
|
margin-bottom: 48px;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-list {
|
.leaderboard-list {
|
||||||
@@ -73,19 +81,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rank-item {
|
.rank-item {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 50px 1fr auto auto;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
gap: 16px;
|
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
padding: 16px;
|
padding: 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border-left: 4px solid var(--teal-light);
|
border-left: 4px solid var(--amber-light);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ligne du joueur courant dans le classement */
|
||||||
|
.rank-item.is-me {
|
||||||
|
border-left-color: var(--amber-dark);
|
||||||
|
background: linear-gradient(135deg, var(--amber-light), var(--white));
|
||||||
|
box-shadow: 0 2px 8px rgba(186, 117, 23, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.rank-item::before {
|
.rank-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -93,14 +114,14 @@
|
|||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(29, 158, 117, 0.1), transparent);
|
background: linear-gradient(90deg, transparent, rgba(186, 117, 23, 0.1), transparent);
|
||||||
transition: left 0.5s ease;
|
transition: left 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-item:hover {
|
.rank-item:hover {
|
||||||
transform: translateX(8px);
|
transform: translateX(8px);
|
||||||
box-shadow: 0 4px 12px rgba(15, 110, 86, 0.15);
|
box-shadow: 0 4px 12px rgba(186, 117, 23, 0.15);
|
||||||
border-left-color: var(--teal-mid);
|
border-left-color: var(--amber-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-item:hover::before {
|
.rank-item:hover::before {
|
||||||
@@ -111,35 +132,21 @@
|
|||||||
.rank-number {
|
.rank-number {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--teal-mid);
|
color: var(--amber-mid);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-number.medal-1 {
|
.rank-number.medal-1,
|
||||||
font-size: 24px;
|
.rank-number.medal-2,
|
||||||
}
|
.rank-number.medal-3 {
|
||||||
|
font-size: 22px;
|
||||||
.rank-number.medal-1::before {
|
|
||||||
content: '🥇';
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-number.medal-2::before {
|
|
||||||
content: '🥈';
|
|
||||||
display: block;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-number.medal-3::before {
|
|
||||||
content: '🥉';
|
|
||||||
display: block;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Infos joueur */
|
/* Infos joueur */
|
||||||
.rank-info {
|
.rank-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-name {
|
.player-name {
|
||||||
@@ -147,17 +154,20 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-stats {
|
.rank-meta {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-stats span {
|
.rank-meta span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -166,39 +176,11 @@
|
|||||||
/* Score et difficulté */
|
/* Score et difficulté */
|
||||||
.rank-score {
|
.rank-score {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--teal-dark);
|
color: var(--amber-dark);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
min-width: 60px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
flex-shrink: 0;
|
||||||
|
|
||||||
.rank-difficulty {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--teal-light);
|
|
||||||
color: var(--teal-dark);
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-difficulty.easy {
|
|
||||||
background: #C8E6C9;
|
|
||||||
color: #2E7D32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-difficulty.normal {
|
|
||||||
background: var(--teal-light);
|
|
||||||
color: var(--teal-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-difficulty.hard {
|
|
||||||
background: #FFCCBC;
|
|
||||||
color: #D84315;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message si pas de scores */
|
/* Message si pas de scores */
|
||||||
@@ -227,32 +209,32 @@
|
|||||||
PLAYER STATS SECTION
|
PLAYER STATS SECTION
|
||||||
============================================ */
|
============================================ */
|
||||||
.player-stats {
|
.player-stats {
|
||||||
background: linear-gradient(135deg, rgba(15, 110, 86, 0.05), transparent);
|
background: linear-gradient(135deg, rgba(186, 117, 23, 0.05), transparent);
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
padding: 32px;
|
padding: 24px;
|
||||||
border: 2px solid var(--teal-light);
|
border: 2px solid var(--amber-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-stats h3 {
|
.player-stats h3 {
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--teal);
|
color: var(--amber);
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border: 2px solid var(--teal-light);
|
border: 2px solid var(--amber-light);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
padding: 14px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -263,27 +245,27 @@
|
|||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-color: var(--teal-mid);
|
border-color: var(--amber-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
font-size: 32px;
|
font-size: 22px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 28px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -291,7 +273,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-card.high-value .stat-value {
|
.stat-card.high-value .stat-value {
|
||||||
font-size: 32px;
|
font-size: 22px;
|
||||||
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +306,7 @@
|
|||||||
RESPONSIVE DESIGN
|
RESPONSIVE DESIGN
|
||||||
============================================ */
|
============================================ */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.leaderboard-section h2 {
|
.leaderboard-panel h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,15 +319,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-item {
|
|
||||||
grid-template-columns: 40px 1fr auto;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-difficulty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-number {
|
.rank-number {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -355,29 +328,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-stats {
|
.player-stats {
|
||||||
padding: 24px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-stats h3 {
|
.player-stats h3 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
padding: 16px;
|
padding: 14px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,17 +370,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rank-item {
|
.rank-item {
|
||||||
grid-template-columns: 40px 1fr;
|
gap: 8px;
|
||||||
gap: 10px;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-score {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-number {
|
.rank-number {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -421,22 +387,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-stats h3 {
|
.player-stats h3 {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
padding: 12px;
|
padding: 12px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Gamification Colors */
|
/* Gamification Colors */
|
||||||
--success: #4CAF50;
|
--success: var(--teal-mid);
|
||||||
--warning: #FF9800;
|
--warning: #FF9800;
|
||||||
--info: #2196F3;
|
--info: #2196F3;
|
||||||
--accent: #FFD700;
|
--accent: #FFD700;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows (alignées sur le langage visuel du site) */
|
||||||
--shadow-sm: 0 4px 8px rgba(15, 110, 86, 0.1);
|
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
--shadow-md: 0 8px 16px rgba(15, 110, 86, 0.15);
|
--shadow-md: 0 12px 32px rgba(0, 0, 0, 0.1);
|
||||||
--shadow-lg: 0 16px 32px rgba(15, 110, 86, 0.25);
|
--shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
============================================ */
|
============================================ */
|
||||||
main {
|
main {
|
||||||
margin-top: 70px;
|
margin-top: 70px;
|
||||||
padding: 40px;
|
padding: 96px 40px 40px;
|
||||||
min-height: calc(100vh - 70px);
|
min-height: calc(100vh - 70px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-container {
|
.game-container {
|
||||||
max-width: 1200px;
|
max-width: 1440px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,15 +34,16 @@ main {
|
|||||||
INTRO SECTION
|
INTRO SECTION
|
||||||
============================================ */
|
============================================ */
|
||||||
.intro-section {
|
.intro-section {
|
||||||
margin-bottom: 60px;
|
margin-bottom: 48px;
|
||||||
animation: fadeInDown 0.6s ease-out;
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-section h1 {
|
.intro-section h1 {
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
font-size: clamp(28px, 5vw, 48px);
|
font-size: clamp(28px, 5vw, 44px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--teal);
|
color: var(--amber);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -56,11 +57,10 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.intro-box {
|
.intro-box {
|
||||||
background: linear-gradient(135deg, var(--teal-light), var(--white));
|
background: linear-gradient(135deg, var(--amber-light), var(--white));
|
||||||
border-left: 4px solid var(--teal-mid);
|
border-left: 4px solid var(--amber-mid);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 32px;
|
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -70,93 +70,148 @@ main {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TOP BAR (niveau + stats en temps réel)
|
||||||
|
============================================ */
|
||||||
|
.game-top-bar {
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 0 auto 32px;
|
||||||
|
background: var(--white);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px clamp(20px, 3vw, 36px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: clamp(20px, 3vw, 40px);
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
GAME LAYOUT (classement / jeu / stats)
|
||||||
|
============================================ */
|
||||||
|
.game-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 320px) minmax(0, 760px) minmax(0, 320px);
|
||||||
|
grid-template-areas: "leaderboard game stats";
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
gap: 32px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-panel {
|
||||||
|
grid-area: leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
|
grid-area: stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
GAME SECTION / PANEL
|
||||||
|
============================================ */
|
||||||
|
.game-section {
|
||||||
|
grid-area: game;
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.3s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-panel {
|
||||||
|
background: var(--white);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: clamp(20px, 4vw, 32px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
DIFFICULTY SELECTOR
|
DIFFICULTY SELECTOR
|
||||||
============================================ */
|
============================================ */
|
||||||
.difficulty-selector {
|
.difficulty-selector {
|
||||||
margin-bottom: 48px;
|
text-align: left;
|
||||||
text-align: center;
|
flex-shrink: 0;
|
||||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-selector label {
|
.difficulty-selector label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.1em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group {
|
.btn-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
grid-template-columns: repeat(3, minmax(100px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-difficulty {
|
.btn-difficulty {
|
||||||
background: var(--white);
|
background: var(--amber-light);
|
||||||
border: 2px solid var(--teal-light);
|
background: linear-gradient(135deg, rgba(250, 238, 218, 0.5), transparent);
|
||||||
border-radius: 8px;
|
border: 2px solid transparent;
|
||||||
padding: 16px 12px;
|
border-radius: 12px;
|
||||||
|
padding: 14px 12px;
|
||||||
font-family: 'Syne', sans-serif;
|
font-family: 'Syne', sans-serif;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-difficulty:hover {
|
.btn-difficulty:hover {
|
||||||
border-color: var(--teal-mid);
|
border-color: var(--amber-mid);
|
||||||
transform: translateY(-4px);
|
transform: translateY(-3px);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-difficulty.active {
|
.btn-difficulty.active {
|
||||||
background: linear-gradient(135deg, var(--teal-light), var(--white));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
border-color: var(--teal-mid);
|
border-color: var(--amber-dark);
|
||||||
color: var(--teal);
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-difficulty.active .btn-label,
|
||||||
|
.btn-difficulty.active .btn-desc {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label {
|
.btn-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--teal);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-desc {
|
.btn-desc {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-family: 'DM Sans', sans-serif;
|
font-family: 'DM Sans', sans-serif;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
GAME SECTION
|
|
||||||
============================================ */
|
|
||||||
.game-section {
|
|
||||||
margin-bottom: 80px;
|
|
||||||
animation: fadeInUp 0.6s ease-out 0.3s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
GAME GRID & CARDS
|
GAME GRID & CARDS
|
||||||
============================================ */
|
============================================ */
|
||||||
.game-grid {
|
.game-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(clamp(70px, 15vw, 140px), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(clamp(70px, 15vw, 110px), 1fr));
|
||||||
gap: clamp(12px, 2vw, 20px);
|
gap: clamp(10px, 2vw, 16px);
|
||||||
max-width: 900px;
|
margin: 0 auto 32px;
|
||||||
margin: 0 auto 40px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: linear-gradient(135deg, rgba(225, 245, 238, 0.4), transparent);
|
background: linear-gradient(135deg, rgba(250, 238, 218, 0.4), transparent);
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--teal-light);
|
border: 1px solid var(--amber-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MEMORY CARD STYLES */
|
/* MEMORY CARD STYLES */
|
||||||
@@ -188,16 +243,19 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-face.front {
|
.card-face.front {
|
||||||
background: linear-gradient(135deg, var(--teal-light), var(--teal-light));
|
background:
|
||||||
border: 2px solid var(--teal-mid);
|
radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.6), transparent 60%),
|
||||||
color: var(--teal-dark);
|
linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
|
border: 2px solid var(--amber-dark);
|
||||||
|
color: var(--white);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-face.back {
|
.card-face.back {
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
background: linear-gradient(135deg, var(--white), var(--teal-light));
|
background: linear-gradient(135deg, var(--white), var(--amber-light));
|
||||||
border: 2px solid var(--teal-mid);
|
border: 2px solid var(--amber-mid);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -246,7 +304,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.memory-card.matched .card-face.back {
|
.memory-card.matched .card-face.back {
|
||||||
background: linear-gradient(135deg, var(--white), rgba(76, 175, 80, 0.2));
|
background: linear-gradient(135deg, var(--white), rgba(29, 158, 117, 0.2));
|
||||||
border-color: var(--success);
|
border-color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,26 +329,16 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
BUTTONS
|
BUTTONS (métriques héritées du style général du site)
|
||||||
============================================ */
|
============================================ */
|
||||||
.btn {
|
.btn {
|
||||||
font-family: 'Syne', sans-serif;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, var(--teal-mid), var(--teal-dark));
|
background: linear-gradient(135deg, var(--amber-mid), var(--amber-dark));
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,21 +349,49 @@ footer {
|
|||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border: 2px solid var(--teal-mid);
|
border: 2px solid var(--amber-mid);
|
||||||
color: var(--teal-mid);
|
color: var(--amber-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: var(--teal-light);
|
background: var(--amber-light);
|
||||||
border-color: var(--teal-dark);
|
border-color: var(--amber-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
RESPONSIVE DESIGN
|
RESPONSIVE DESIGN
|
||||||
============================================ */
|
============================================ */
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.game-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"game"
|
||||||
|
"leaderboard"
|
||||||
|
"stats";
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.game-top-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-selector {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
main {
|
main {
|
||||||
padding: 20px;
|
padding: 56px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-section h1 {
|
.intro-section h1 {
|
||||||
@@ -345,7 +421,7 @@ footer {
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
main {
|
main {
|
||||||
padding: 16px;
|
padding: 40px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-section h1 {
|
.intro-section h1 {
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ class GameSessionState {
|
|||||||
this.moves = 0;
|
this.moves = 0;
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
this.matched = 0;
|
this.matched = 0;
|
||||||
this.isPaused = false;
|
|
||||||
this.gameActive = false;
|
this.gameActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +189,6 @@ class GameSessionState {
|
|||||||
this.score = 0;
|
this.score = 0;
|
||||||
this.matched = 0;
|
this.matched = 0;
|
||||||
this.gameActive = true;
|
this.gameActive = true;
|
||||||
this.isPaused = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,89 +1,113 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
LEADERBOARD & PLAYER STATISTICS
|
LEADERBOARD & PLAYER STATISTICS
|
||||||
|
============================================
|
||||||
|
Le CLASSEMENT est désormais partagé EN LIGNE : tous les joueurs
|
||||||
|
enregistrent leurs scores via l'API PHP (api/scores.php) et voient le
|
||||||
|
même top 10 par mode de jeu.
|
||||||
|
|
||||||
|
Les STATISTIQUES PERSONNELLES (« Vos statistiques ») restent stockées
|
||||||
|
localement dans le navigateur du joueur, car le serveur ne conserve que
|
||||||
|
le top 10 de chaque mode (et non l'historique complet de chacun).
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager du classement
|
* Manager du classement
|
||||||
*/
|
*/
|
||||||
class LeaderboardManager {
|
class LeaderboardManager {
|
||||||
static KEY_LEADERBOARD = 'nextgn_leaderboard';
|
// Point d'entrée de l'API (relatif à memory.html) — ASP.NET + SQL Server
|
||||||
static MAX_SCORES = 100;
|
static API_URL = 'api/scores.ashx';
|
||||||
|
// Historique local des parties du joueur (pour ses stats perso)
|
||||||
|
static KEY_HISTORY = 'nextgn_player_history';
|
||||||
|
static MAX_HISTORY = 500;
|
||||||
|
|
||||||
|
// Cache mémoire du dernier classement récupéré (par difficulté)
|
||||||
|
static _cache = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ajouter un score au classement
|
* Enregistrer une partie :
|
||||||
|
* - envoie le score au classement en ligne
|
||||||
|
* - conserve la partie dans l'historique local (stats perso)
|
||||||
|
* @returns {Promise<Object>} résultat de l'API ({ ok, isTop, rank } ou { ok:false })
|
||||||
*/
|
*/
|
||||||
static addScore(gameData) {
|
static async addScore(gameData) {
|
||||||
const scores = this.getAllScores();
|
const record = {
|
||||||
|
|
||||||
const newScore = {
|
|
||||||
sessionId: generateUUID(),
|
|
||||||
playerName: getPlayerName(),
|
playerName: getPlayerName(),
|
||||||
userId: getUserId(),
|
userId: getUserId(),
|
||||||
score: gameData.score,
|
score: gameData.score,
|
||||||
moves: gameData.moves,
|
moves: gameData.moves,
|
||||||
timeSeconds: gameData.duration,
|
timeSeconds: gameData.duration,
|
||||||
difficulty: gameData.difficulty,
|
difficulty: gameData.difficulty,
|
||||||
date: new Date().toISOString(),
|
|
||||||
isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires,
|
isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires,
|
||||||
won: gameData.won !== false
|
won: gameData.won !== false,
|
||||||
|
date: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
scores.push(newScore);
|
// 1) Historique local (statistiques personnelles)
|
||||||
|
this._recordLocalHistory(record);
|
||||||
|
|
||||||
// Limiter à MAX_SCORES (garder les plus récents)
|
// 2) Classement en ligne
|
||||||
if (scores.length > this.MAX_SCORES) {
|
try {
|
||||||
scores.sort((a, b) => new Date(b.date) - new Date(a.date));
|
const res = await fetch(this.API_URL, {
|
||||||
scores.splice(this.MAX_SCORES);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(record)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
DEBUG.log('Score submitted:', data);
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Leaderboard submit error:', e);
|
||||||
|
return { ok: false, error: 'network' };
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(this.KEY_LEADERBOARD, JSON.stringify(scores));
|
|
||||||
|
|
||||||
DEBUG.log('Score added:', newScore);
|
|
||||||
|
|
||||||
return newScore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir tous les scores
|
* Ajouter la partie à l'historique local du joueur
|
||||||
*/
|
*/
|
||||||
static getAllScores() {
|
static _recordLocalHistory(record) {
|
||||||
|
const history = this._getLocalHistory();
|
||||||
|
history.push(record);
|
||||||
|
if (history.length > this.MAX_HISTORY) {
|
||||||
|
history.splice(0, history.length - this.MAX_HISTORY);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const scores = JSON.parse(localStorage.getItem(this.KEY_LEADERBOARD)) || [];
|
localStorage.setItem(this.KEY_HISTORY, JSON.stringify(history));
|
||||||
return scores;
|
} catch (e) {
|
||||||
|
console.error('History save error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Historique local des parties du joueur
|
||||||
|
*/
|
||||||
|
static _getLocalHistory() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(this.KEY_HISTORY)) || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Leaderboard error:', e);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir le classement (top X)
|
* Récupérer le classement en ligne (top X) pour un mode donné.
|
||||||
|
* @returns {Promise<Array>}
|
||||||
*/
|
*/
|
||||||
static getLeaderboard(difficulty = 'all', limit = 10) {
|
static async getLeaderboard(difficulty = 'all', limit = 10) {
|
||||||
let scores = this.getAllScores();
|
const url = `${this.API_URL}?difficulty=${encodeURIComponent(difficulty)}&limit=${limit}`;
|
||||||
|
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||||
// Filtrer par difficulté
|
const data = await res.json();
|
||||||
if (difficulty !== 'all') {
|
if (!data || data.ok !== true || !Array.isArray(data.scores)) {
|
||||||
scores = scores.filter(s => s.difficulty === difficulty);
|
throw new Error('Réponse invalide du serveur');
|
||||||
}
|
}
|
||||||
|
this._cache[difficulty] = data.scores;
|
||||||
// Trier par score (décroissant)
|
return data.scores;
|
||||||
scores.sort((a, b) => {
|
|
||||||
if (b.score !== a.score) {
|
|
||||||
return b.score - a.score;
|
|
||||||
}
|
|
||||||
// En cas d'égalité, par temps
|
|
||||||
return a.timeSeconds - b.timeSeconds;
|
|
||||||
});
|
|
||||||
|
|
||||||
return scores.slice(0, limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir les stats d'un joueur
|
* Statistiques du joueur, calculées à partir de son historique LOCAL.
|
||||||
*/
|
*/
|
||||||
static getPlayerStats(userId) {
|
static getPlayerStats() {
|
||||||
const scores = this.getAllScores().filter(s => s.userId === userId);
|
const scores = this._getLocalHistory();
|
||||||
|
|
||||||
if (scores.length === 0) {
|
if (scores.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -116,61 +140,35 @@ class LeaderboardManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir le rang d'un score
|
* Mettre à jour l'affichage du classement (asynchrone).
|
||||||
*/
|
*/
|
||||||
static getPlayerRank(userId, difficulty = 'all') {
|
static async updateDisplay(difficulty = 'all') {
|
||||||
const leaderboard = this.getLeaderboard(difficulty, Infinity);
|
|
||||||
const rank = leaderboard.findIndex(s => s.userId === userId) + 1;
|
|
||||||
return rank > 0 ? rank : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifier si c'est un nouveau record personnel
|
|
||||||
*/
|
|
||||||
static isPersonalRecord(userId, newScore) {
|
|
||||||
const stats = this.getPlayerStats(userId);
|
|
||||||
return newScore > stats.bestScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtenir les statistiques globales
|
|
||||||
*/
|
|
||||||
static getGlobalStats() {
|
|
||||||
const scores = this.getAllScores();
|
|
||||||
|
|
||||||
if (scores.length === 0) {
|
|
||||||
return {
|
|
||||||
totalGames: 0,
|
|
||||||
totalPlayers: 0,
|
|
||||||
averageScore: 0,
|
|
||||||
highestScore: 0,
|
|
||||||
fastestCompletion: Infinity
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniquePlayers = new Set(scores.map(s => s.userId));
|
|
||||||
const totalScore = scores.reduce((a, b) => a + b.score, 0);
|
|
||||||
const highestScore = Math.max(...scores.map(s => s.score), 0);
|
|
||||||
const fastestCompletion = Math.min(...scores.map(s => s.timeSeconds), Infinity);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalGames: scores.length,
|
|
||||||
totalPlayers: uniquePlayers.size,
|
|
||||||
averageScore: Math.round(totalScore / scores.length),
|
|
||||||
highestScore: highestScore,
|
|
||||||
fastestCompletion: fastestCompletion === Infinity ? 0 : fastestCompletion
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mettre à jour l'affichage du classement
|
|
||||||
*/
|
|
||||||
static updateDisplay(difficulty = 'all') {
|
|
||||||
const leaderboard = this.getLeaderboard(difficulty, 10);
|
|
||||||
const leaderboardList = document.getElementById('leaderboard-list');
|
const leaderboardList = document.getElementById('leaderboard-list');
|
||||||
|
|
||||||
if (!leaderboardList) return;
|
if (!leaderboardList) return;
|
||||||
|
|
||||||
|
// État de chargement
|
||||||
|
leaderboardList.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">⏳</div>
|
||||||
|
<div class="empty-state-text">Chargement du classement…</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let leaderboard;
|
||||||
|
try {
|
||||||
|
leaderboard = await this.getLeaderboard(difficulty, 10);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Leaderboard load error:', e);
|
||||||
|
leaderboardList.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">⚠️</div>
|
||||||
|
<div class="empty-state-text">Classement indisponible</div>
|
||||||
|
<div class="empty-state-hint">Vérifiez votre connexion et réessayez.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (leaderboard.length === 0) {
|
if (leaderboard.length === 0) {
|
||||||
leaderboardList.innerHTML = `
|
leaderboardList.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -182,118 +180,80 @@ class LeaderboardManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myUserId = getUserId();
|
||||||
leaderboardList.innerHTML = '';
|
leaderboardList.innerHTML = '';
|
||||||
|
|
||||||
leaderboard.forEach((score, index) => {
|
leaderboard.forEach((score, index) => {
|
||||||
const rankItem = document.createElement('div');
|
const rankItem = document.createElement('div');
|
||||||
rankItem.className = 'rank-item';
|
rankItem.className = 'rank-item';
|
||||||
|
if (score.userId === myUserId) {
|
||||||
|
rankItem.classList.add('is-me');
|
||||||
|
}
|
||||||
|
|
||||||
const rankNumber = this.getRankDisplay(index);
|
const rankNumber = this.getRankDisplay(index);
|
||||||
const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"';
|
const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"';
|
||||||
|
|
||||||
rankItem.innerHTML = `
|
rankItem.innerHTML = `
|
||||||
|
<div class="rank-top">
|
||||||
<div ${medal}>${rankNumber}</div>
|
<div ${medal}>${rankNumber}</div>
|
||||||
|
<div class="rank-score">${formatNumber(score.score)}</div>
|
||||||
|
</div>
|
||||||
<div class="rank-info">
|
<div class="rank-info">
|
||||||
<div class="player-name">${this.escapeHTML(score.playerName)}</div>
|
<div class="player-name">${this.escapeHTML(score.playerName)}</div>
|
||||||
<div class="player-stats">
|
<div class="rank-meta">
|
||||||
<span>⏱ ${formatTime(score.timeSeconds)}</span>
|
<span>⏱ ${formatTime(score.timeSeconds)}</span>
|
||||||
<span>🎯 ${score.moves} mouvements</span>
|
<span>🎯 ${score.moves}</span>
|
||||||
${score.isPerfect ? '<span>✨ Parfait</span>' : ''}
|
${score.isPerfect ? '<span>✨</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rank-score">${formatNumber(score.score)}</div>
|
|
||||||
<div class="rank-difficulty ${score.difficulty}">${DIFFICULTY_MODES[score.difficulty].name}</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
leaderboardList.appendChild(rankItem);
|
leaderboardList.appendChild(rankItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof analyticsManager !== 'undefined') {
|
||||||
analyticsManager.track('leaderboard_viewed', { filter: difficulty });
|
analyticsManager.track('leaderboard_viewed', { filter: difficulty });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mettre à jour les statistiques du joueur
|
* Mettre à jour les statistiques du joueur (locales).
|
||||||
*/
|
*/
|
||||||
static updatePlayerStats() {
|
static updatePlayerStats() {
|
||||||
const userId = getUserId();
|
const stats = this.getPlayerStats();
|
||||||
const stats = this.getPlayerStats(userId);
|
|
||||||
|
|
||||||
// Remplir les éléments HTML
|
const totalGamesEl = document.getElementById('total-games');
|
||||||
document.getElementById('total-games').textContent = stats.totalGames;
|
const bestScoreEl = document.getElementById('best-score');
|
||||||
document.getElementById('best-score').textContent = formatNumber(stats.bestScore);
|
const bestTimeEl = document.getElementById('best-time');
|
||||||
document.getElementById('best-time').textContent = stats.bestTime === Infinity ? '--' : formatTime(stats.bestTime);
|
const winRateEl = document.getElementById('win-rate');
|
||||||
document.getElementById('win-rate').textContent = `${stats.winRate}%`;
|
|
||||||
|
|
||||||
// Ajouter des classes pour animation si valeurs élevées
|
if (totalGamesEl) totalGamesEl.textContent = stats.totalGames;
|
||||||
if (stats.bestScore > 5000) {
|
if (bestScoreEl) bestScoreEl.textContent = formatNumber(stats.bestScore);
|
||||||
document.getElementById('best-score').parentElement.classList.add('high-value');
|
if (bestTimeEl) bestTimeEl.textContent = stats.bestTime === Infinity || stats.bestTime === 0 ? '--' : formatTime(stats.bestTime);
|
||||||
|
if (winRateEl) winRateEl.textContent = `${stats.winRate}%`;
|
||||||
|
|
||||||
|
if (bestScoreEl && stats.bestScore > 5000) {
|
||||||
|
bestScoreEl.parentElement.classList.add('high-value');
|
||||||
}
|
}
|
||||||
|
if (winRateEl && stats.winRate >= 50) {
|
||||||
if (stats.winRate >= 50) {
|
winRateEl.parentElement.classList.add('high-value');
|
||||||
document.getElementById('win-rate').parentElement.classList.add('high-value');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Afficher le rang du joueur
|
* Effacer l'historique local du joueur (DEBUG).
|
||||||
|
* N'affecte PAS le classement en ligne partagé.
|
||||||
*/
|
*/
|
||||||
static updatePlayerRank(difficulty = 'all') {
|
static clearLocalHistory() {
|
||||||
const userId = getUserId();
|
if (confirm('Effacer VOS statistiques locales ? (le classement en ligne n\'est pas touché)')) {
|
||||||
const rank = this.getPlayerRank(userId, difficulty);
|
localStorage.removeItem(this.KEY_HISTORY);
|
||||||
|
|
||||||
if (rank) {
|
|
||||||
const rankDisplay = document.getElementById('player-rank');
|
|
||||||
if (rankDisplay) {
|
|
||||||
rankDisplay.textContent = `Vous êtes #{rank}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Afficher les statistiques globales
|
|
||||||
*/
|
|
||||||
static displayGlobalStats() {
|
|
||||||
const globalStats = this.getGlobalStats();
|
|
||||||
DEBUG.log('Global Statistics:', globalStats);
|
|
||||||
|
|
||||||
return globalStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nettoyer les scores (DEBUG)
|
|
||||||
*/
|
|
||||||
static clearAll() {
|
|
||||||
if (confirm('Êtes-vous ABSOLUMENT certain de vouloir supprimer TOUS les scores?')) {
|
|
||||||
localStorage.removeItem(this.KEY_LEADERBOARD);
|
|
||||||
this.updateDisplay();
|
|
||||||
this.updatePlayerStats();
|
this.updatePlayerStats();
|
||||||
DEBUG.log('✓ Tous les scores ont été supprimés');
|
DEBUG.log('✓ Historique local effacé');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exporter les scores en CSV
|
|
||||||
*/
|
|
||||||
static exportCSV() {
|
|
||||||
const scores = this.getAllScores();
|
|
||||||
|
|
||||||
let csv = 'Rang,Joueur,Score,Difficulté,Temps (s),Mouvements,Parfait,Date\n';
|
|
||||||
|
|
||||||
scores.forEach((score, index) => {
|
|
||||||
csv += `${index + 1},"${score.playerName}",${score.score},"${score.difficulty}",${score.timeSeconds},${score.moves},${score.isPerfect ? 'Oui' : 'Non'},"${score.date}"\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `leaderboard-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir l'affichage du rang (numéro ou médaille)
|
* Obtenir l'affichage du rang (numéro ou médaille)
|
||||||
*/
|
*/
|
||||||
@@ -312,78 +272,11 @@ class LeaderboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistiques avancées
|
|
||||||
*/
|
|
||||||
class AdvancedStats {
|
|
||||||
/**
|
|
||||||
* Obtenir la distribution des scores par difficulté
|
|
||||||
*/
|
|
||||||
static getDistributionByDifficulty() {
|
|
||||||
const scores = LeaderboardManager.getAllScores();
|
|
||||||
const distribution = {};
|
|
||||||
|
|
||||||
Object.keys(DIFFICULTY_MODES).forEach(difficulty => {
|
|
||||||
distribution[difficulty] = scores.filter(s => s.difficulty === difficulty).length;
|
|
||||||
});
|
|
||||||
|
|
||||||
return distribution;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtenir la tendance temporelle
|
|
||||||
*/
|
|
||||||
static getTimelineTrend(days = 7) {
|
|
||||||
const scores = LeaderboardManager.getAllScores();
|
|
||||||
const now = new Date();
|
|
||||||
const timeline = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < days; i++) {
|
|
||||||
const date = new Date(now);
|
|
||||||
date.setDate(date.getDate() - i);
|
|
||||||
const dateStr = date.toISOString().slice(0, 10);
|
|
||||||
timeline[dateStr] = scores.filter(s => s.date.slice(0, 10) === dateStr).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtenir la moyenne de temps par difficulté
|
|
||||||
*/
|
|
||||||
static getAverageTimeByDifficulty() {
|
|
||||||
const scores = LeaderboardManager.getAllScores();
|
|
||||||
const averages = {};
|
|
||||||
|
|
||||||
Object.keys(DIFFICULTY_MODES).forEach(difficulty => {
|
|
||||||
const diffScores = scores.filter(s => s.difficulty === difficulty);
|
|
||||||
if (diffScores.length > 0) {
|
|
||||||
const totalTime = diffScores.reduce((a, b) => a + b.timeSeconds, 0);
|
|
||||||
averages[difficulty] = Math.round(totalTime / diffScores.length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return averages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Taux de réussite global
|
|
||||||
*/
|
|
||||||
static getGlobalSuccessRate() {
|
|
||||||
const scores = LeaderboardManager.getAllScores();
|
|
||||||
if (scores.length === 0) return 0;
|
|
||||||
|
|
||||||
const perfectGames = scores.filter(s => s.isPerfect).length;
|
|
||||||
return Math.round((perfectGames / scores.length) * 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// INITIALISATION
|
// INITIALISATION
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Exporter pour utilisation globale
|
// Exporter pour utilisation globale
|
||||||
window.LeaderboardManager = LeaderboardManager;
|
window.LeaderboardManager = LeaderboardManager;
|
||||||
window.AdvancedStats = AdvancedStats;
|
|
||||||
|
|
||||||
DEBUG.log('Leaderboard Manager initialized');
|
DEBUG.log('Leaderboard Manager initialized (online mode)');
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class MemoryGameEngine {
|
|||||||
moves: 0,
|
moves: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
isPaused: false,
|
|
||||||
gameStarted: false
|
gameStarted: false
|
||||||
};
|
};
|
||||||
this.startTime = null;
|
this.startTime = null;
|
||||||
@@ -128,7 +127,6 @@ class MemoryGameEngine {
|
|||||||
if (this.gameState.flipped.length === 2) return;
|
if (this.gameState.flipped.length === 2) return;
|
||||||
if (this.cards[cardIndex].isMatched) return;
|
if (this.cards[cardIndex].isMatched) return;
|
||||||
if (this.gameState.flipped.includes(cardIndex)) return;
|
if (this.gameState.flipped.includes(cardIndex)) return;
|
||||||
if (this.gameState.isPaused) return;
|
|
||||||
|
|
||||||
// Retourner la carte
|
// Retourner la carte
|
||||||
this.flipCard(cardIndex);
|
this.flipCard(cardIndex);
|
||||||
@@ -260,9 +258,7 @@ class MemoryGameEngine {
|
|||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
|
||||||
this.timerInterval = setInterval(() => {
|
this.timerInterval = setInterval(() => {
|
||||||
if (!this.gameState.isPaused) {
|
|
||||||
this.updateTimer();
|
this.updateTimer();
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
this.analytics.trackGameStart(this.difficulty);
|
this.analytics.trackGameStart(this.difficulty);
|
||||||
@@ -351,10 +347,14 @@ class MemoryGameEngine {
|
|||||||
achievements: achievements.map(a => a.id)
|
achievements: achievements.map(a => a.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sauvegarder score
|
// Sauvegarder le score (classement en ligne) puis rafraîchir l'affichage
|
||||||
LeaderboardManager.addScore({
|
LeaderboardManager.addScore({
|
||||||
...gameData,
|
...gameData,
|
||||||
score: finalScore.totalScore
|
score: finalScore.totalScore
|
||||||
|
}).then(() => {
|
||||||
|
const activeFilter = document.querySelector('.tab-btn.active')?.getAttribute('data-filter') || 'all';
|
||||||
|
LeaderboardManager.updateDisplay(activeFilter);
|
||||||
|
LeaderboardManager.updatePlayerStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Afficher modal victoire
|
// Afficher modal victoire
|
||||||
@@ -429,7 +429,6 @@ class MemoryGameEngine {
|
|||||||
moves: 0,
|
moves: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
isPaused: false,
|
|
||||||
gameStarted: false
|
gameStarted: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,24 +443,6 @@ class MemoryGameEngine {
|
|||||||
document.getElementById('timer-display').style.animation = '';
|
document.getElementById('timer-display').style.animation = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause/Reprise
|
|
||||||
*/
|
|
||||||
togglePause() {
|
|
||||||
if (!this.gameState.gameStarted) return;
|
|
||||||
|
|
||||||
this.gameState.isPaused = !this.gameState.isPaused;
|
|
||||||
const pauseBtn = document.getElementById('pause-btn');
|
|
||||||
|
|
||||||
if (this.gameState.isPaused) {
|
|
||||||
pauseBtn.textContent = '▶ Reprendre';
|
|
||||||
pauseBtn.classList.add('paused');
|
|
||||||
} else {
|
|
||||||
pauseBtn.textContent = '⏸ Pause';
|
|
||||||
pauseBtn.classList.remove('paused');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup event listeners
|
* Setup event listeners
|
||||||
*/
|
*/
|
||||||
@@ -469,9 +450,6 @@ class MemoryGameEngine {
|
|||||||
// Bouton restart
|
// Bouton restart
|
||||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||||
|
|
||||||
// Bouton pause
|
|
||||||
document.getElementById('pause-btn').addEventListener('click', () => this.togglePause());
|
|
||||||
|
|
||||||
// Sélecteur de difficulté
|
// Sélecteur de difficulté
|
||||||
document.querySelectorAll('.btn-difficulty').forEach(btn => {
|
document.querySelectorAll('.btn-difficulty').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
@@ -512,14 +490,51 @@ class MemoryGameEngine {
|
|||||||
// ============================================
|
// ============================================
|
||||||
let game;
|
let game;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
/**
|
||||||
// Initialiser le jeu
|
* Demander le nom du joueur à la première partie
|
||||||
game = new MemoryGameEngine('normal');
|
*/
|
||||||
|
function setupPlayerNameModal(onReady) {
|
||||||
|
const nameConfirmed = localStorage.getItem('nextgn_name_confirmed') === 'true';
|
||||||
|
|
||||||
|
if (nameConfirmed) {
|
||||||
|
onReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('player-name-modal');
|
||||||
|
const input = document.getElementById('player-name-input');
|
||||||
|
const confirmBtn = document.getElementById('player-name-confirm');
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
const confirmName = () => {
|
||||||
|
const name = input.value.trim();
|
||||||
|
localStorage.setItem('nextgn_playerName', name || getPlayerName());
|
||||||
|
localStorage.setItem('nextgn_name_confirmed', 'true');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
onReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmBtn.addEventListener('click', confirmName, { once: true });
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') confirmName();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Afficher le leaderboard initial
|
// Afficher le leaderboard initial
|
||||||
LeaderboardManager.updateDisplay('all');
|
LeaderboardManager.updateDisplay('all');
|
||||||
LeaderboardManager.updatePlayerStats();
|
LeaderboardManager.updatePlayerStats();
|
||||||
|
|
||||||
|
setupPlayerNameModal(() => {
|
||||||
|
LeaderboardManager.updateDisplay('all');
|
||||||
|
LeaderboardManager.updatePlayerStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialiser le jeu
|
||||||
|
game = new MemoryGameEngine('normal');
|
||||||
|
|
||||||
// Scroll progress
|
// Scroll progress
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
game.updateHUD();
|
game.updateHUD();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
nom: "Gaz comprimé",
|
nom: "Gaz comprimé",
|
||||||
filename: "1-gaz-comprime.png",
|
filename: "sous_pression.jpg",
|
||||||
texte: "Je suis sous pression",
|
texte: "Je suis sous pression",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -17,15 +17,15 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
nom: "Exclamation",
|
nom: "Exclamation",
|
||||||
filename: "2-exclamation.png",
|
filename: "nocif_irritant_ozone.jpg",
|
||||||
texte: "Je nuis gravement à la santé",
|
texte: "J'altère la santé ou la couche d'ozone",
|
||||||
danger: "Danger pour la santé",
|
danger: "Danger pour la santé/Environnement",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
nom: "Explosion",
|
nom: "Explosion",
|
||||||
filename: "3-explosion.png",
|
filename: "explosif.jpg",
|
||||||
texte: "J'explose",
|
texte: "J'explose",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -33,7 +33,7 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
nom: "Crâne",
|
nom: "Crâne",
|
||||||
filename: "4-crane.png",
|
filename: "toxicite_aigue.jpg",
|
||||||
texte: "Je tue",
|
texte: "Je tue",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -41,7 +41,7 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
nom: "Flamme",
|
nom: "Flamme",
|
||||||
filename: "5-flamme.png",
|
filename: "inflammable.jpg",
|
||||||
texte: "Je flambe",
|
texte: "Je flambe",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -49,7 +49,7 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
nom: "Comburant",
|
nom: "Comburant",
|
||||||
filename: "6-comburant.png",
|
filename: "comburant.jpg",
|
||||||
texte: "Je fais flamber",
|
texte: "Je fais flamber",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -57,7 +57,7 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
nom: "Environnement",
|
nom: "Environnement",
|
||||||
filename: "7-environnement.png",
|
filename: "pollution.jpg",
|
||||||
texte: "Je pollue",
|
texte: "Je pollue",
|
||||||
danger: "Danger pour l'environnement",
|
danger: "Danger pour l'environnement",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
@@ -65,17 +65,17 @@ const PICTOGRAMMES = [
|
|||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
nom: "Corrosion",
|
nom: "Corrosion",
|
||||||
filename: "8-corrosion.png",
|
filename: "corrosif.jpg",
|
||||||
texte: "Je ronge",
|
texte: "Je ronge",
|
||||||
danger: "Danger physique",
|
danger: "Danger physique",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
nom: "Santé/Environnement",
|
nom: "Santé (CMR)",
|
||||||
filename: "9-sante-environnement.png",
|
filename: "cmr.jpg",
|
||||||
texte: "J'altère la santé ou la couche d'ozone",
|
texte: "Je nuis gravement à la santé",
|
||||||
danger: "Danger pour la santé/Environnement",
|
danger: "Danger pour la santé",
|
||||||
couleur: "teal"
|
couleur: "teal"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -292,10 +292,10 @@ function getRank(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifier si le score est dans le top 10
|
* Vérifier si le score est dans le top 10 (classement en ligne, asynchrone)
|
||||||
*/
|
*/
|
||||||
function isTopScore(score, difficulty = 'all') {
|
async function isTopScore(score, difficulty = 'all') {
|
||||||
const scores = LeaderboardManager.getLeaderboard(difficulty, 10);
|
const scores = await LeaderboardManager.getLeaderboard(difficulty, 10);
|
||||||
return scores.some(s => s.score === score.score && s.userId === score.userId);
|
return scores.some(s => s.score === score.score && s.userId === score.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,57 +318,25 @@ function formatDate(isoDate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtenir les statistiques d'un joueur
|
* Obtenir les statistiques locales du joueur
|
||||||
*/
|
*/
|
||||||
function getPlayerStatistics(userId) {
|
function getPlayerStatistics() {
|
||||||
const scores = LeaderboardManager.getAllScores().filter(s => s.userId === userId);
|
return LeaderboardManager.getPlayerStats();
|
||||||
|
|
||||||
if (scores.length === 0) {
|
|
||||||
return {
|
|
||||||
totalGames: 0,
|
|
||||||
bestScore: 0,
|
|
||||||
averageScore: 0,
|
|
||||||
averageTime: 0,
|
|
||||||
bestTime: Infinity,
|
|
||||||
winRate: 0,
|
|
||||||
perfectGames: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const bestScore = Math.max(...scores.map(s => s.score), 0);
|
|
||||||
const totalScore = scores.reduce((a, b) => a + b.score, 0);
|
|
||||||
const totalTime = scores.reduce((a, b) => a + b.timeSeconds, 0);
|
|
||||||
const bestTime = Math.min(...scores.map(s => s.timeSeconds), Infinity);
|
|
||||||
const perfectGames = scores.filter(s => s.isPerfect).length;
|
|
||||||
const winRate = (perfectGames / scores.length) * 100;
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalGames: scores.length,
|
|
||||||
bestScore: bestScore,
|
|
||||||
averageScore: Math.round(totalScore / scores.length),
|
|
||||||
averageTime: Math.round(totalTime / scores.length),
|
|
||||||
bestTime: bestTime === Infinity ? 0 : bestTime,
|
|
||||||
winRate: Math.round(winRate),
|
|
||||||
perfectGames: perfectGames
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déboguer : afficher tous les scores en console
|
* Déboguer : afficher l'historique local du joueur en console
|
||||||
*/
|
*/
|
||||||
function debugScores() {
|
function debugScores() {
|
||||||
console.table(LeaderboardManager.getAllScores());
|
console.table(LeaderboardManager._getLocalHistory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déboguer : nettoyer tous les scores
|
* Déboguer : effacer les statistiques locales du joueur
|
||||||
|
* (le classement en ligne partagé n'est pas affecté)
|
||||||
*/
|
*/
|
||||||
function clearAllScores() {
|
function clearAllScores() {
|
||||||
if (confirm('Êtes-vous sûr de vouloir supprimer tous les scores?')) {
|
LeaderboardManager.clearLocalHistory();
|
||||||
localStorage.removeItem('nextgn_leaderboard');
|
|
||||||
console.log('✓ Tous les scores ont été supprimés');
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Memory Pictogrammes GHS - NextGN Formation</title>
|
<title>Memory Pictogrammes GHS - Gauthier Chombart & Nathan Chauwin</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="../../header-style.css"/>
|
<link rel="stylesheet" href="../../header-style.css"/>
|
||||||
<link rel="stylesheet" href="assets/css/memory-game.css">
|
<link rel="stylesheet" href="assets/css/memory-game.css?v=3">
|
||||||
<link rel="stylesheet" href="assets/css/hud.css">
|
<link rel="stylesheet" href="assets/css/hud.css?v=3">
|
||||||
<link rel="stylesheet" href="assets/css/animations.css">
|
<link rel="stylesheet" href="assets/css/animations.css?v=3">
|
||||||
<link rel="stylesheet" href="assets/css/leaderboard.css">
|
<link rel="stylesheet" href="assets/css/leaderboard.css?v=3">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- HEADER NextGN Formation -->
|
<!-- HEADER Gauthier Chombart & Nathan Chauwin -->
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="../../index.html">Accueil</a>
|
<a href="../../index.html">Accueil</a>
|
||||||
<a href="../../index.html#formations">Exercices</a>
|
<a href="../../index.html#formations">Exercices</a>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<main class="game-container">
|
<main class="game-container">
|
||||||
<!-- INTRO SECTION -->
|
<!-- INTRO SECTION -->
|
||||||
<section class="intro-section">
|
<section class="intro-section">
|
||||||
<h1>🎮 Memory Pictogrammes GHS</h1>
|
<h1>Memory pictogrammes</h1>
|
||||||
<p class="subtitle">Maîtrisez les 9 symboles de danger</p>
|
<p class="subtitle">Maîtrisez les 9 symboles de danger</p>
|
||||||
|
|
||||||
<div class="intro-box">
|
<div class="intro-box">
|
||||||
@@ -45,72 +45,57 @@
|
|||||||
Plus rapide vous êtes, plus de points vous gagnez! 🏆
|
Plus rapide vous êtes, plus de points vous gagnez! 🏆
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TOP BAR (niveau + stats en temps réel) -->
|
||||||
|
<div class="game-top-bar">
|
||||||
<!-- DIFFICULTY SELECTOR -->
|
<!-- DIFFICULTY SELECTOR -->
|
||||||
<div class="difficulty-selector">
|
<div class="difficulty-selector">
|
||||||
<label>Choisir le niveau :</label>
|
<label>Choisir le niveau</label>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-difficulty" data-difficulty="easy">
|
<button class="btn btn-difficulty" data-difficulty="easy">
|
||||||
|
<span class="btn-icon">🌱</span>
|
||||||
<span class="btn-label">Facile</span>
|
<span class="btn-label">Facile</span>
|
||||||
<span class="btn-desc">4 paires</span>
|
<span class="btn-desc">4 paires</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-difficulty active" data-difficulty="normal">
|
<button class="btn btn-difficulty active" data-difficulty="normal">
|
||||||
|
<span class="btn-icon">🎯</span>
|
||||||
<span class="btn-label">Normal</span>
|
<span class="btn-label">Normal</span>
|
||||||
<span class="btn-desc">9 paires</span>
|
<span class="btn-desc">9 paires</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-difficulty" data-difficulty="hard">
|
<button class="btn btn-difficulty" data-difficulty="hard">
|
||||||
|
<span class="btn-icon">🔥</span>
|
||||||
<span class="btn-label">Difficile</span>
|
<span class="btn-label">Difficile</span>
|
||||||
<span class="btn-desc">2 min chrono</span>
|
<span class="btn-desc">2 min chrono</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- GAME SECTION -->
|
|
||||||
<section class="game-section">
|
|
||||||
<!-- HUD (Stats en temps réel) -->
|
<!-- HUD (Stats en temps réel) -->
|
||||||
<div class="game-hud">
|
<div class="game-hud">
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<div class="label">Score</div>
|
<div class="label">🏆 Score</div>
|
||||||
<div class="value" id="score-display">0</div>
|
<div class="value" id="score-display">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<div class="label">Mouvements</div>
|
<div class="label">👆 Mouvements</div>
|
||||||
<div class="value" id="moves-display">0</div>
|
<div class="value" id="moves-display">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<div class="label">Temps</div>
|
<div class="label">⏱ Temps</div>
|
||||||
<div class="value" id="timer-display">00:00</div>
|
<div class="value" id="timer-display">00:00</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<div class="label">Paires</div>
|
<div class="label">🧩 Paires</div>
|
||||||
<div class="value" id="pairs-display">0/9</div>
|
<div class="value" id="pairs-display">0/9</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PROGRESS BAR -->
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar" id="progress-bar"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- GAME GRID -->
|
<!-- GAME LAYOUT (classement / jeu / statistiques) -->
|
||||||
<div class="game-grid" id="game-grid">
|
<div class="game-layout">
|
||||||
<!-- Cartes générées par JS -->
|
<!-- LEADERBOARD PANEL (gauche) -->
|
||||||
</div>
|
<aside class="leaderboard-panel">
|
||||||
|
|
||||||
<!-- CONTROLS -->
|
|
||||||
<div class="game-controls">
|
|
||||||
<button class="btn btn-primary" id="restart-btn">
|
|
||||||
🔄 Recommencer
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" id="pause-btn">
|
|
||||||
⏸ Pause
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- LEADERBOARD SECTION -->
|
|
||||||
<section class="leaderboard-section">
|
|
||||||
<h2>🏆 Classement</h2>
|
<h2>🏆 Classement</h2>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
@@ -125,8 +110,32 @@
|
|||||||
<!-- Rempli par JS -->
|
<!-- Rempli par JS -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- PLAYER STATS -->
|
<!-- GAME SECTION (centre) -->
|
||||||
|
<section class="game-section">
|
||||||
|
<div class="game-panel">
|
||||||
|
<!-- PROGRESS BAR -->
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="game-progress-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GAME GRID -->
|
||||||
|
<div class="game-grid" id="game-grid">
|
||||||
|
<!-- Cartes générées par JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTROLS -->
|
||||||
|
<div class="game-controls">
|
||||||
|
<button class="btn btn-primary" id="restart-btn">
|
||||||
|
🔄 Recommencer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PLAYER STATS PANEL (droite) -->
|
||||||
|
<aside class="stats-panel">
|
||||||
<div class="player-stats">
|
<div class="player-stats">
|
||||||
<h3>Vos statistiques</h3>
|
<h3>Vos statistiques</h3>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
@@ -152,9 +161,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</aside>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- PLAYER NAME MODAL (1ère partie) -->
|
||||||
|
<div class="victory-modal hidden" id="player-name-modal">
|
||||||
|
<div class="modal-content name-modal-content">
|
||||||
|
<h2>Bienvenue ! 👋</h2>
|
||||||
|
<p class="name-modal-text">
|
||||||
|
Entrez votre nom ou un pseudo pour apparaître dans le classement général.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="player-name-input"
|
||||||
|
class="name-input"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="Votre nom ou pseudo"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" id="player-name-confirm">
|
||||||
|
🎮 Commencer à jouer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- VICTORY MODAL -->
|
<!-- VICTORY MODAL -->
|
||||||
<div class="victory-modal hidden" id="victory-modal">
|
<div class="victory-modal hidden" id="victory-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -196,7 +229,7 @@
|
|||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2024 NextGN Formation - Memory Pictogrammes GHS</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés - Formateurs indépendants</p>
|
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés - Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
37
index.html
37
index.html
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>NextGN Formation - Formations Certibiocide & Techniques</title>
|
<title>Gauthier Chombart & Nathan Chauwin - Formations Certibiocide & Techniques</title>
|
||||||
|
|
||||||
<!-- SEO de base -->
|
<!-- SEO de base -->
|
||||||
<meta name="description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts. Exercices, documentation technique sur les nuisibles et rapports de veille réglementaire.">
|
<meta name="description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts. Exercices, documentation technique sur les nuisibles et rapports de veille réglementaire.">
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
<!-- Open Graph (partage sur les réseaux sociaux et LinkedIn) -->
|
<!-- Open Graph (partage sur les réseaux sociaux et LinkedIn) -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://nextgnformation.bsite.net/">
|
<meta property="og:url" content="https://nextgnformation.bsite.net/">
|
||||||
<meta property="og:title" content="NextGN Formation - Formations Certibiocide & Techniques">
|
<meta property="og:title" content="Gauthier Chombart & Nathan Chauwin - Formations Certibiocide & Techniques">
|
||||||
<meta property="og:description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts. Exercices, documentation technique sur les nuisibles et rapports de veille réglementaire.">
|
<meta property="og:description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts. Exercices, documentation technique sur les nuisibles et rapports de veille réglementaire.">
|
||||||
<meta property="og:locale" content="fr_FR">
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
|
||||||
<!-- Twitter Card -->
|
<!-- Twitter Card -->
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:title" content="NextGN Formation - Formations Certibiocide & Techniques">
|
<meta name="twitter:title" content="Gauthier Chombart & Nathan Chauwin - Formations Certibiocide & Techniques">
|
||||||
<meta name="twitter:description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts.">
|
<meta name="twitter:description" content="Formations Certibiocide pratiques et interactives par des formateurs indépendants experts.">
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#formations">Exercices interactifs</a>
|
<a href="#formations">Exercices interactifs</a>
|
||||||
<a href="#documentation">Documentation</a>
|
<a href="#documentation" style="display:none;">Documentation</a>
|
||||||
<a href="#rapports">Rapports de veille</a>
|
<a href="#rapports" style="display:none;">Rapports de veille</a>
|
||||||
<a href="#about">À propos</a>
|
<a href="#about">À propos</a>
|
||||||
<a href="#contact">Contact</a>
|
<a href="#contact">Contact</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
|
|
||||||
<div class="cta-group">
|
<div class="cta-group">
|
||||||
<a href="#formations" class="btn btn-primary">Suivre les exercices interactifs</a>
|
<a href="#formations" class="btn btn-primary">Suivre les exercices interactifs</a>
|
||||||
<a href="#documentation" class="btn btn-secondary">Documentation</a>
|
<a href="#documentation" class="btn btn-secondary" style="display:none;">Documentation</a>
|
||||||
<a href="#rapports" class="btn btn-secondary">Rapports de veille</a>
|
<a href="#rapports" class="btn btn-secondary" style="display:none;">Rapports de veille</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<div class="formation-card">
|
<div class="formation-card">
|
||||||
<div class="formation-header">
|
<div class="formation-header">
|
||||||
<div class="formation-icon">📋</div>
|
<div class="formation-icon">📋</div>
|
||||||
<h3 class="formation-title">FDS & Étiquettes</h3>
|
<h3 class="formation-title">FDS, FT & Étiquettes</h3>
|
||||||
<p class="formation-subtitle">Aller plus loin</p>
|
<p class="formation-subtitle">Aller plus loin</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="formation-body">
|
<div class="formation-body">
|
||||||
@@ -137,11 +137,24 @@
|
|||||||
<a href="exercices/memory-pictogrammes/memory.html" class="formation-link">Jouer</a>
|
<a href="exercices/memory-pictogrammes/memory.html" class="formation-link">Jouer</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Correction : page unique pour tous les exercices -->
|
||||||
|
<div class="formation-card">
|
||||||
|
<div class="formation-header">
|
||||||
|
<div class="formation-icon">✅</div>
|
||||||
|
<h3 class="formation-title">Corriger un exercice</h3>
|
||||||
|
<p class="formation-subtitle">Correction des rendus</p>
|
||||||
|
</div>
|
||||||
|
<div class="formation-body">
|
||||||
|
<p class="formation-description">Déposez le PDF exporté par un groupe et obtenez la lecture de son rendu. Page unique de correction pour l'ensemble des exercices.</p>
|
||||||
|
<a href="exercices/corriger-exercice.html" class="formation-link">Corriger</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- DOCUMENTATION TECHNIQUE SECTION -->
|
<!-- DOCUMENTATION TECHNIQUE SECTION -->
|
||||||
<section id="documentation" class="documentation-section">
|
<section id="documentation" class="documentation-section" style="display:none;">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Documentation technique et biologie des espèces</h2>
|
<h2>Documentation technique et biologie des espèces</h2>
|
||||||
<p>Accédez à notre base de connaissances : études détaillées, analyses scientifiques et rapports techniques sur les nuisibles, les biocides et les enjeux réglementaires.</p>
|
<p>Accédez à notre base de connaissances : études détaillées, analyses scientifiques et rapports techniques sur les nuisibles, les biocides et les enjeux réglementaires.</p>
|
||||||
@@ -178,7 +191,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- RAPPORTS DE VEILLE HEBDOMADAIRES -->
|
<!-- RAPPORTS DE VEILLE HEBDOMADAIRES -->
|
||||||
<section id="rapports" class="rapports-section">
|
<section id="rapports" class="rapports-section" style="display:none;">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Rapports de veille hebdomadaires</h2>
|
<h2>Rapports de veille hebdomadaires</h2>
|
||||||
<p>Suivez chaque semaine l'actualité réglementaire, les opportunités de marché et les tendances du secteur Certibiocide en France et en Europe.</p>
|
<p>Suivez chaque semaine l'actualité réglementaire, les opportunités de marché et les tendances du secteur Certibiocide en France et en Europe.</p>
|
||||||
@@ -314,7 +327,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation - Formations Certibiocide & Techniques</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -419,7 +419,7 @@
|
|||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#formations">Exercices interactifs</a>
|
<a href="#formations">Exercices interactifs</a>
|
||||||
<a href="#documentation">Documentation</a>
|
<a href="#documentation">Documentation</a>
|
||||||
@@ -799,7 +799,7 @@
|
|||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2024 NextGN Formation - Rapport Veille Semaine 20</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p>Tous droits réservés - Formateur indépendant spécialisé en Certibiocide</p>
|
<p>Tous droits réservés - Formateur indépendant spécialisé en Certibiocide</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -419,7 +419,7 @@
|
|||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#formations">Exercices interactifs</a>
|
<a href="#formations">Exercices interactifs</a>
|
||||||
<a href="#documentation">Documentation</a>
|
<a href="#documentation">Documentation</a>
|
||||||
@@ -799,7 +799,7 @@
|
|||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2024 NextGN Formation - Rapport Veille Semaine 21</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p>Tous droits réservés - Formateur indépendant spécialisé en Certibiocide</p>
|
<p>Tous droits réservés - Formateur indépendant spécialisé en Certibiocide</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Veille Certibiocide — Semaine 23 | NextGN Formation</title>
|
<title>Veille Certibiocide — Semaine 23 | Gauthier Chombart & Nathan Chauwin</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
|
|
||||||
<!-- NAV -->
|
<!-- NAV -->
|
||||||
<nav class="nav-header">
|
<nav class="nav-header">
|
||||||
<a href="index.html" class="header-logo"><span>NextGN</span> Formation</a>
|
<a href="index.html" class="header-logo">Gauthier Chombart & Nathan Chauwin</a>
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
<a href="index.html#formations">Exercices interactifs</a>
|
<a href="index.html#formations">Exercices interactifs</a>
|
||||||
<a href="index.html#about">À propos</a>
|
<a href="index.html#about">À propos</a>
|
||||||
@@ -800,7 +800,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation — Veille Certibiocide Semaine 23</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p style="margin-top:10px; opacity:0.6;">Tous droits réservés — Formateurs indépendants</p>
|
<p style="margin-top:10px; opacity:0.6;">Tous droits réservés — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="nav-header">
|
<nav class="nav-header">
|
||||||
<a href="index.html" class="header-logo"><span>NextGN</span> Formation</a>
|
<a href="index.html" class="header-logo">Gauthier Chombart & Nathan Chauwin</a>
|
||||||
<nav class="header-nav" id="mainNav">
|
<nav class="header-nav" id="mainNav">
|
||||||
<a href="index.html#formations">Exercices interactifs</a>
|
<a href="index.html#formations">Exercices interactifs</a>
|
||||||
<a href="index.html#about">À propos</a>
|
<a href="index.html#about">À propos</a>
|
||||||
@@ -790,7 +790,7 @@
|
|||||||
</div><!-- /container -->
|
</div><!-- /container -->
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation — Veille Certibiocide Semaine 24</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p style="margin-top:10px; opacity:0.6;">Tous droits réservés — Formateurs indépendants</p>
|
<p style="margin-top:10px; opacity:0.6;">Tous droits réservés — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -375,7 +375,7 @@
|
|||||||
|
|
||||||
<!-- NAV HEADER -->
|
<!-- NAV HEADER -->
|
||||||
<nav class="nav-header">
|
<nav class="nav-header">
|
||||||
<a href="index.html" class="header-logo"><span>NextGN</span> Formation</a>
|
<a href="index.html" class="header-logo">Gauthier Chombart & Nathan Chauwin</a>
|
||||||
<button class="hamburger" id="hamburger" aria-label="Menu">
|
<button class="hamburger" id="hamburger" aria-label="Menu">
|
||||||
<span></span><span></span><span></span>
|
<span></span><span></span><span></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -704,7 +704,7 @@
|
|||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation - Veille Certibiocide Semaine 25</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p>Tous droits réservés - Formateurs indépendants</p>
|
<p>Tous droits réservés - Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Veille Certibiocide — Semaine 26 | NextGN Formation</title>
|
<title>Veille Certibiocide — Semaine 26 | Gauthier Chombart & Nathan Chauwin</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -378,7 +378,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="nav-header">
|
<nav class="nav-header">
|
||||||
<a href="index.html" class="header-logo"><span>NextGN</span> Formation</a>
|
<a href="index.html" class="header-logo">Gauthier Chombart & Nathan Chauwin</a>
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
<a href="index.html#formations">Exercices interactifs</a>
|
<a href="index.html#formations">Exercices interactifs</a>
|
||||||
<a href="index.html#about">À propos</a>
|
<a href="index.html#about">À propos</a>
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
<h1>Veille Certibiocide<br>Semaine 26 — 2026</h1>
|
<h1>Veille Certibiocide<br>Semaine 26 — 2026</h1>
|
||||||
<p class="hero-meta">
|
<p class="hero-meta">
|
||||||
<strong>Semaine du 23 au 29 juin 2026</strong> ·
|
<strong>Semaine du 23 au 29 juin 2026</strong> ·
|
||||||
Généré le 29 juin 2026 · NextGN Formation
|
Généré le 29 juin 2026 · Gauthier Chombart & Nathan Chauwin
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -810,7 +810,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation — Veille Certibiocide Semaine 26</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p>Tous droits réservés — Formateurs indépendants</p>
|
<p>Tous droits réservés — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
835
veille/rapports/veille-certibiocide-semaine-28.html
Normal file
835
veille/rapports/veille-certibiocide-semaine-28.html
Normal file
@@ -0,0 +1,835 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Veille Certibiocide — Semaine 28 | NextGN Formation</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--teal: #0F6E56;
|
||||||
|
--teal-mid: #1D9E75;
|
||||||
|
--teal-light: #E1F5EE;
|
||||||
|
--teal-dark: #085041;
|
||||||
|
--amber: #BA7517;
|
||||||
|
--amber-light: #FAEEDA;
|
||||||
|
--amber-dark: #633806;
|
||||||
|
--coral: #993C1D;
|
||||||
|
--coral-mid: #D85A30;
|
||||||
|
--coral-light: #FAECE7;
|
||||||
|
--purple: #534AB7;
|
||||||
|
--purple-light: #EEEDFE;
|
||||||
|
--purple-dark: #3C3489;
|
||||||
|
--blue: #185FA5;
|
||||||
|
--blue-light: #E6F1FB;
|
||||||
|
--bg: #F7F6F2;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--text: #1a1a18;
|
||||||
|
--text-muted: #6b6a65;
|
||||||
|
--border: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-top: 70px;
|
||||||
|
background: var(--bg);
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PROGRESS BAR ── */
|
||||||
|
.progress {
|
||||||
|
height: 3px;
|
||||||
|
background: var(--border);
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--teal-mid);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── NAV HEADER ── */
|
||||||
|
.nav-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 70px;
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 40px;
|
||||||
|
}
|
||||||
|
.header-logo {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
background: linear-gradient(135deg, var(--teal), var(--coral-mid));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
.header-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
.header-nav a {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
.header-nav a::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--teal-mid);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
.header-nav a:hover::after { width: 100%; }
|
||||||
|
.header-nav a:hover { color: var(--teal-mid); }
|
||||||
|
|
||||||
|
/* ── HERO ── */
|
||||||
|
.hero {
|
||||||
|
background: var(--teal-light);
|
||||||
|
border-bottom: 2px solid var(--teal-mid);
|
||||||
|
padding: 60px 40px 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--teal-mid);
|
||||||
|
color: var(--white);
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(28px, 4vw, 46px);
|
||||||
|
color: var(--teal-dark);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
.hero-meta {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.hero-meta strong { color: var(--teal); font-weight: 500; }
|
||||||
|
|
||||||
|
/* ── CONTAINER ── */
|
||||||
|
.container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── SOMMAIRE ── */
|
||||||
|
.sommaire {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
padding: 28px 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.sommaire h2 {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.sommaire-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.sommaire-grid a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--teal-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.sommaire-grid a:hover {
|
||||||
|
background: var(--teal-light);
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── POINTS D'ATTENTION ── */
|
||||||
|
.attention-box {
|
||||||
|
background: var(--amber-light);
|
||||||
|
border-left: 4px solid var(--amber);
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
padding: 28px 32px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.attention-box h2 {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.attention-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.attention-item:last-child { margin-bottom: 0; }
|
||||||
|
.attention-num {
|
||||||
|
background: var(--amber);
|
||||||
|
color: var(--white);
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.attention-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.attention-text strong { font-weight: 500; }
|
||||||
|
|
||||||
|
/* ── SECTIONS RUBRIQUES ── */
|
||||||
|
.section {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
margin-bottom: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
background: var(--teal-light);
|
||||||
|
border-bottom: 1px solid rgba(29,158,117,0.2);
|
||||||
|
padding: 20px 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.section-emoji { font-size: 22px; }
|
||||||
|
.section-header h2 {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--teal-dark);
|
||||||
|
}
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 300;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.section-body { padding: 24px 28px; }
|
||||||
|
|
||||||
|
/* ── ACTUALITÉ CARD ── */
|
||||||
|
.actu {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.actu:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
||||||
|
.actu-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||||
|
.source-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--teal-mid);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.actu-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.actu p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.actu a {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--teal-mid);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.actu a:hover { transform: translateY(-1px); color: var(--teal-dark); }
|
||||||
|
.actu a::before { content: '→ '; }
|
||||||
|
|
||||||
|
/* ── NOTE AVERTISSEMENT ── */
|
||||||
|
.warning-note {
|
||||||
|
background: var(--amber-light);
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FOOTER ── */
|
||||||
|
footer {
|
||||||
|
background: var(--text);
|
||||||
|
color: var(--white);
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
footer p { margin-bottom: 4px; }
|
||||||
|
|
||||||
|
/* ── RESPONSIVE ── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-header { padding: 0 20px; }
|
||||||
|
.header-nav { gap: 14px; }
|
||||||
|
.header-nav a { font-size: 10px; }
|
||||||
|
.hero { padding: 40px 20px 36px; }
|
||||||
|
.container { padding: 24px 16px; }
|
||||||
|
.section-header { padding: 16px 20px; }
|
||||||
|
.section-body { padding: 18px 20px; }
|
||||||
|
.sommaire { padding: 20px; }
|
||||||
|
.attention-box { padding: 20px; }
|
||||||
|
.sommaire-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-nav { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Barre de progression -->
|
||||||
|
<div class="progress"><div class="progress-bar" id="progressBar"></div></div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="nav-header">
|
||||||
|
<a href="index.html" class="header-logo"><span>NextGN</span> Formation</a>
|
||||||
|
<nav class="header-nav">
|
||||||
|
<a href="index.html#formations">Exercices interactifs</a>
|
||||||
|
<a href="index.html#about">À propos</a>
|
||||||
|
<a href="index.html#contact">Contact</a>
|
||||||
|
</nav>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Hero -->
|
||||||
|
<header class="hero">
|
||||||
|
<div class="hero-badge">Rapport de veille · Semaine 28</div>
|
||||||
|
<h1>Veille Certibiocide</h1>
|
||||||
|
<p class="hero-meta">
|
||||||
|
<strong>Semaine du 7 au 13 juillet 2026</strong> ·
|
||||||
|
8 rubriques · Sources officielles prioritaires
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Sommaire -->
|
||||||
|
<nav class="sommaire" aria-label="Sommaire">
|
||||||
|
<h2>Sommaire</h2>
|
||||||
|
<div class="sommaire-grid">
|
||||||
|
<a href="#reglementaire">⚖️ Réglementaire</a>
|
||||||
|
<a href="#formation">🎓 Marché formation</a>
|
||||||
|
<a href="#nuisibles">🐀 Nuisibles & ESOD</a>
|
||||||
|
<a href="#desinfection">🧴 Désinfection</a>
|
||||||
|
<a href="#opportunites">💡 Opportunités</a>
|
||||||
|
<a href="#innovation">🔬 Innovation</a>
|
||||||
|
<a href="#europe">🇪🇺 Veille européenne</a>
|
||||||
|
<a href="#rh">👥 RH & Concurrence</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Points d'attention -->
|
||||||
|
<div class="attention-box">
|
||||||
|
<h2>📌 Points d'attention — Semaine 28</h2>
|
||||||
|
|
||||||
|
<div class="attention-item">
|
||||||
|
<div class="attention-num">1</div>
|
||||||
|
<div class="attention-text">
|
||||||
|
<strong>Qualiopi FAFCEA obligatoire depuis le 1er juillet 2026.</strong>
|
||||||
|
Les organismes non certifiés Qualiopi ne peuvent plus accéder aux financements FAFCEA (chefs d'entreprise artisanale). Point critique à vérifier si Certibiocide est dispensée à des artisans.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attention-item">
|
||||||
|
<div class="attention-num">2</div>
|
||||||
|
<div class="attention-text">
|
||||||
|
<strong>Réforme Omnibus BPR : vers une approbation indéfinie des substances actives.</strong>
|
||||||
|
La Commission européenne propose de supprimer le renouvellement obligatoire tous les 10 ans pour les substances ne relevant pas des critères d'exclusion. Changement structurel majeur à intégrer dans les contenus de formation réglementaire.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attention-item">
|
||||||
|
<div class="attention-num">3</div>
|
||||||
|
<div class="attention-text">
|
||||||
|
<strong>Moustique tigre dans 83 départements (86 % du territoire).</strong>
|
||||||
|
La progression continue crée une forte demande de formation désinsectisation/démoustication. Opportunité de développement de modules spécifiques pour les professionnels 3D.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── 1. RÉGLEMENTAIRE ─── -->
|
||||||
|
<section class="section" id="reglementaire">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">⚖️</span>
|
||||||
|
<div>
|
||||||
|
<h2>Réglementaire</h2>
|
||||||
|
<div class="section-subtitle">ANSES · ECHA · MTE · Légifrance</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ANSES</span>
|
||||||
|
<span class="actu-date">Publié le 01/07/2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Nouvelle AMM — ZANZA SPRAY autorisé.</strong> L'ANSES a signé le 18 juin 2026 la décision d'autorisation de mise à disposition sur le marché du produit biocide ZANZA SPRAY, publiée au registre le 1er juillet 2026. Ce produit rejoint la liste des répulsifs ou insecticides biocides homologués en France.</p>
|
||||||
|
<a href="https://www.anses.fr/fr/decisions_biocide" target="_blank" rel="noopener">Registre AMM biocides ANSES</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ANSES</span>
|
||||||
|
<span class="actu-date">Publiés le 24/06/2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Retraits d'AMM — 5 produits de traitement du bois.</strong> Cinq produits biocides ont fait l'objet d'une décision de retrait d'autorisation signée le 15/06/2026 et publiée le 24/06/2026 : TRAITEMENT DU BOIS MULTI USAGES, WINKO TRAITEMENT BOIS UNIVERSEL, XILIX 7000, XYL SORX 2000 et EKOLIS TRAITEMENT BOIS MULTI USAGES. Les professionnels concernés doivent procéder à la liquidation de leurs stocks dans les délais impartis.</p>
|
||||||
|
<a href="https://www.anses.fr/fr/decisions_biocide" target="_blank" rel="noopener">Registre AMM biocides ANSES</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ANSES</span>
|
||||||
|
<span class="actu-date">2026 (en cours)</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Consultation publique — lignes directrices AMM biocides.</strong> L'ANSES a mis en consultation publique sa proposition de lignes directrices pour la délivrance des AMM biocides en France. Ce document vise à clarifier et standardiser le processus d'instruction des dossiers d'autorisation pour les fabricants et importateurs.</p>
|
||||||
|
<a href="https://www.anses.fr/fr/content/lanses-met-en-consultation-publique-sa-proposition-de-lignes-directrices-pour-la-delivrance" target="_blank" rel="noopener">Consultation ANSES</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Ecologie.gouv.fr</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Rappel réglementaire — Certification obligatoire 2026 renforcée.</strong> L'année 2026 marque un renforcement de la réglementation biocide en France : la certification Certibiocide reste obligatoire pour tout professionnel utilisant des produits biocides à usage professionnel. Les OF délivrant la formation doivent eux-mêmes être certifiés Qualiopi pour accéder aux fonds publics.</p>
|
||||||
|
<a href="https://www.ecologie.gouv.fr/politiques-publiques/produits-biocides" target="_blank" rel="noopener">MTE — Produits biocides</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 2. MARCHÉ FORMATION ─── -->
|
||||||
|
<section class="section" id="formation">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">🎓</span>
|
||||||
|
<div>
|
||||||
|
<h2>Marché formation</h2>
|
||||||
|
<div class="section-subtitle">France Compétences · Mon Compte Formation · OPCO EP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">France Compétences</span>
|
||||||
|
<span class="actu-date">1er juillet 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Qualiopi obligatoire pour FAFCEA dès le 1er juillet 2026.</strong> Depuis le 1er juillet 2026, seuls les organismes de formation certifiés Qualiopi sont éligibles aux financements FAFCEA (Fonds d'Assurance Formation des Chefs d'Entreprise Artisanale). Point critique pour les formateurs Certibiocide intervenant auprès d'artisans plombiers, carreleurs ou autres professions exposées.</p>
|
||||||
|
<a href="https://csfc-federation.org/blog/qualiopi-devient-obligatoire-pour-les-financements-fafcea-des-2026/" target="_blank" rel="noopener">CSFC-Fédération</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">IGAS / OPCO</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Budget OPCO 2026 — 11,8 Mds€ gérés, 521M€ pour les TPE/PME.</strong> Selon les données IGAS publiées en novembre 2025, les 11 OPCO gèrent 11,8 milliards d'euros de ressources en 2026. L'enveloppe dédiée au plan de développement des compétences des entreprises de moins de 50 salariés atteint 521 millions d'euros — un levier important pour les formations Certibiocide en entreprise.</p>
|
||||||
|
<a href="https://www.starthack.fr/financement-de-la-formation-professionnelle-en-2026-opco-budgets-et-obligations-des-centres-de-formation/" target="_blank" rel="noopener">Analyse financement 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">France Compétences</span>
|
||||||
|
<span class="actu-date">Janvier 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Renouvellement des 8 instances de labellisation Qualiopi jusqu'en 2028.</strong> France Compétences a renouvelé pour trois ans (à compter du 1er janvier 2026) les huit certificateurs habilités à délivrer la certification Qualiopi. Les OF certifiés n'ont pas à rechanger d'organisme certificateur avant 2028.</p>
|
||||||
|
<a href="https://www.digi-certif.com/debut-2026-quelles-actualites-pour-la-formation-pro-et-qualiopi/" target="_blank" rel="noopener">Actualités Qualiopi 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">OPCO</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Délais d'instruction OPCO : 15 à 30 jours.</strong> En 2026, le délai moyen entre le dépôt d'un dossier de financement et la notification se situe entre 15 et 30 jours selon les OPCO. Un délai à anticiper dans la planification des sessions de formation Certibiocide pour les clients entreprises.</p>
|
||||||
|
<a href="https://formaltic.fr/financement-opco-formations-2026/" target="_blank" rel="noopener">Financement OPCO 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 3. NUISIBLES & ESOD ─── -->
|
||||||
|
<section class="section" id="nuisibles">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">🐀</span>
|
||||||
|
<div>
|
||||||
|
<h2>Nuisibles & ESOD</h2>
|
||||||
|
<div class="section-subtitle">ANSES · INRAE · OFB · Santé Publique France</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Santé Publique France</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Moustique tigre — 83 départements colonisés (86 % du territoire).</strong> Début 2026, <em>Aedes albopictus</em> est implanté de façon permanente dans 83 des 96 départements de métropole. L'espèce représente un risque sanitaire croissant avec des transmissions locales de dengue et de chikungunya, amplifiées par le réchauffement climatique. Juillet et août sont les mois de pic d'activité.</p>
|
||||||
|
<a href="https://www.service-public.gouv.fr/particuliers/actualites/A14140" target="_blank" rel="noopener">Service-public.fr — Moustique tigre</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">IZIPest / OFB</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Frelon asiatique — 70 départements touchés, 20 % de la mortalité apicole.</strong> En 2026, le frelon asiatique (<em>Vespa velutina</em>) est présent dans plus de 70 départements français. Il contribue à environ 20 % de la mortalité des colonies d'abeilles domestiques. Le frelon asiatique reste classé ESOD (Espèce Susceptible d'Occasionner des Dégâts) dans toutes les régions concernées.</p>
|
||||||
|
<a href="https://izipest.com/blog/top-3-especes-envahissantes-france-2026/" target="_blank" rel="noopener">IZIPest — Espèces invasives 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ANSES / INRAE</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Punaises de lit — enjeu socio-économique majeur persistant.</strong> Les punaises de lit (<em>Cimex lectularius</em>) ne transmettent pas de maladie mais génèrent un impact psychologique, social et économique important. Leur élimination reste coûteuse et techniquement exigeante, nécessitant une intervention certifiée. Le niveau de demande d'intervention professionnelle reste élevé en 2026.</p>
|
||||||
|
<a href="https://www.larochedeglun.fr/Lutte-contre-le-frelon-asiatique-moustique-tigre-Punaises" target="_blank" rel="noopener">Info espèces nuisibles</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">IZIPest</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Fourmi d'Argentine — 3e espèce invasive préoccupante en 2026.</strong> Aux côtés du moustique tigre et du frelon asiatique, la fourmi d'Argentine (<em>Linepithema humile</em>) est identifiée parmi les espèces invasives les plus préoccupantes pour les professionnels de la lutte antiparasitaire. Sa présence dans les zones urbaines et péri-urbaines du Sud de la France est en progression.</p>
|
||||||
|
<a href="https://izipest.com/blog/top-3-especes-envahissantes-france-2026/" target="_blank" rel="noopener">IZIPest — Top 3 espèces 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning-note">
|
||||||
|
⚠️ <em>Aucune publication officielle vérifiée cette semaine sur les rongeurs résistants aux rodenticides (ANSES, INRAE) pour la période juillet 2026. Données issues des sources les plus récentes disponibles.</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 4. DÉSINFECTION ─── -->
|
||||||
|
<section class="section" id="desinfection">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">🧴</span>
|
||||||
|
<div>
|
||||||
|
<h2>Désinfection</h2>
|
||||||
|
<div class="section-subtitle">ANSES · AFNOR</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">AFNOR / ANSES</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Renforcement de la certification obligatoire pour les professionnels de la désinfection.</strong> En 2026, la réglementation biocide française renforce l'obligation pour tout professionnel manipulant des désinfectants biocides d'être titulaire du Certibiocide. Ce renforcement est documenté sur plusieurs sites de formation spécialisés (TBCF, Delcourt) à la suite de nouvelles exigences réglementaires entrées en vigueur début 2026.</p>
|
||||||
|
<a href="https://tbcformation.com/certibiocide-2026-nouveautes-reglementation-desinfection/" target="_blank" rel="noopener">TBCF — Certibiocide 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">AFNOR</span>
|
||||||
|
<span class="actu-date">En vigueur</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Norme NF T 72-281 — Désinfection des surfaces par voie aérienne.</strong> La norme NF T 72-281 (AFNOR) encadre les dispositifs de désinfection par fumigation et nébulisation. Elle reste la référence technique pour les professionnels 3D intervenant sur des surfaces étendues. Les produits biocides utilisés dans ce cadre doivent impérativement disposer d'une AMM valide.</p>
|
||||||
|
<a href="https://normalisation.afnor.org/actualites/antiseptiques-et-desinfectants-chimiques-afnor-vous-eclaire-sur-lapport-des-normes-volontaires/" target="_blank" rel="noopener">AFNOR — Normes désinfectants</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">AFNOR</span>
|
||||||
|
<span class="actu-date">En vigueur</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>NF EN 14885 — Norme cadre des produits biocides en désinfection.</strong> La NF EN 14885 est la norme européenne régissant les produits biocides pour la désinfection en milieux hospitalier, industriel, agroalimentaire et communautaire. Elle définit les conditions d'évaluation de l'efficacité anti-microbienne (bactéricide, fongicide, virucide, sporicide). Point de référence incontournable en formation Certibiocide TP 19.</p>
|
||||||
|
<a href="https://www.boutique.afnor.org/en-gb/standard/nf-en-14476/chemical-disinfectants-and-antiseptics-quantitative-suspension-test-for-the/fa166895/1398" target="_blank" rel="noopener">AFNOR — NF EN 14476</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ANSES / UE</span>
|
||||||
|
<span class="actu-date">En vigueur</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Rappel — Règlement UE 528/2012 : enregistrement obligatoire de tous les désinfectants biocides.</strong> Tout désinfectant commercialisé doit être enregistré conformément au règlement BPR (UE) 528/2012. L'autorisation est délivrée par l'autorité nationale compétente (ANSES en France), garantissant sécurité et efficacité pour l'usage revendiqué. Ce cadre doit être rappelé systématiquement en formation.</p>
|
||||||
|
<a href="https://www.prodhybase.fr/norme_generalite.html" target="_blank" rel="noopener">ProdHYBase — Normes</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 5. OPPORTUNITÉS ─── -->
|
||||||
|
<section class="section" id="opportunites">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">💡</span>
|
||||||
|
<div>
|
||||||
|
<h2>Opportunités</h2>
|
||||||
|
<div class="section-subtitle">BOAMP · CPF · Secteurs demandeurs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">BOAMP</span>
|
||||||
|
<span class="actu-date">Juillet 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>AO Département de l'Oise — Missions 3D sur sites et réseau routier.</strong> Un marché public significatif pour des missions de dératisation, désinsectisation, désinfection et traitements spécifiques sur les sites et le réseau routier du département de l'Oise (réf. 3/boamp/2616972). La date prévisionnelle de notification du marché-cadre est fixée à juillet 2026. Ce type de marché nécessite des intervenants titulaires du Certibiocide.</p>
|
||||||
|
<a href="https://www.francemarches.com/appel-offre/3boamp2616972-2026-missions-deratisation-desinsectisation" target="_blank" rel="noopener">AO Oise — FranceMarchés</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">BOAMP</span>
|
||||||
|
<span class="actu-date">2026 (en cours)</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>AO Prestations anti-nuisibles 4D — Marché pluriannuel.</strong> Plusieurs marchés 4D (dératisation, désinsectisation, désinfection, dépigeonnisation) sont actuellement ouverts sur BOAMP (réf. 3/boamp/2627622 et 3/boamp/264816). Ces marchés publiés incluent des prestations de prévention et de lutte contre les nuisibles et l'insalubrité, ciblant les collectivités et établissements publics.</p>
|
||||||
|
<a href="https://www.francemarches.com/appels-offre/deratisation-desinsectisation" target="_blank" rel="noopener">81 AO dératisation-désinsectisation en cours</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">BOAMP</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>AO Ville de Fos-sur-Mer — Lutte contre les nuisibles en 2 lots.</strong> La ville de Fos-sur-Mer a publié un appel d'offres en 2026 pour des prestations de lutte antiparasitaire en deux lots : (1) dératisation/désinsectisation/désinfection classique et (2) campagne de lutte contre les rats par prédation. Ce second lot illustre l'intérêt croissant pour les méthodes alternatives aux rodenticides chimiques.</p>
|
||||||
|
<a href="https://www.francemarches.com/appel-offre/3boamp2651311-2026-prestations-lutte-contre" target="_blank" rel="noopener">AO Fos-sur-Mer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">FranceMarchés</span>
|
||||||
|
<span class="actu-date">Juillet 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>94 AO dératisation et 81 AO désinsectisation-désinfection actifs en France.</strong> Au moment de la veille (semaine 28), FranceMarchés recense 94 appels d'offres actifs pour la dératisation seule et 81 pour la dématisation-désinsectisation. Ce volume confirme un marché dynamique et une demande soutenue en professionnels certifiés Certibiocide.</p>
|
||||||
|
<a href="https://www.francemarches.com/appels-offre/deratisation" target="_blank" rel="noopener">FranceMarchés — 94 AO dératisation</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 6. INNOVATION ─── -->
|
||||||
|
<section class="section" id="innovation">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">🔬</span>
|
||||||
|
<div>
|
||||||
|
<h2>Innovation</h2>
|
||||||
|
<div class="section-subtitle">CORDIS · PubMed · BPI France</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">CORDIS / UE</span>
|
||||||
|
<span class="actu-date">2024–2027</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Projet LIFE BioPest — Bioinsecticides fongiques pour remplacer les pyréthroïdes urbains.</strong> Le projet européen LIFE BioPest (2024-2027) finance le développement de bioinsecticides à base de champignons entomopathogènes destinés à remplacer les pyréthroïdes en milieu urbain. Ce projet vise directement les pratiques des techniciens 3D et pourrait modifier les produits disponibles en TP 18 (désinsectisation).</p>
|
||||||
|
<a href="https://www.mdpi.com/2077-0472/16/5/597" target="_blank" rel="noopener">MDPI — Contrôle biologique des insectes 2026</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Haaretz / R&D</span>
|
||||||
|
<span class="actu-date">Mai 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>IA pour la conception de bioinsecticides protéiques — 1er proof-of-concept mondial.</strong> Une entreprise a annoncé en mai 2026 avoir atteint un jalon critique : la mise au point du premier proof-of-concept mondial d'une plateforme de conception de bioinsecticides protéiques pilotée par IA. Cette approche de novo permet de cibler des espèces nuisibles spécifiques avec une haute sélectivité et sans résistance croisée.</p>
|
||||||
|
<a href="https://www.haaretz.com/haaretz-labels/cutting-edge/2026-05-25/ty-article-labels/.premium/pioneering-next-generation-crop-protection/0000019e-5e92-d6cc-a79e-dfbe61750000" target="_blank" rel="noopener">Haaretz — Next-Gen Crop Protection</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Anticimex / IoT</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Pièges connectés et surveillance IoT temps réel — la lutte antiparasitaire 2.0.</strong> Les solutions de lutte antiparasitaire connectées (pièges intelligents, détecteurs automatiques, tableaux de bord IoT) se déploient en industrie agroalimentaire et grande distribution. Ces dispositifs permettent une prévention proactive plutôt qu'une intervention curative. Le formateur Certibiocide doit intégrer ces outils dans les modules sur les méthodes alternatives.</p>
|
||||||
|
<a href="https://www.anticimex.fr/blog/lutte-nuisibles-connectee-ia-iot-data/" target="_blank" rel="noopener">Anticimex — IA, IoT & Data</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 7. VEILLE EUROPÉENNE ─── -->
|
||||||
|
<section class="section" id="europe">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">🇪🇺</span>
|
||||||
|
<div>
|
||||||
|
<h2>Veille européenne</h2>
|
||||||
|
<div class="section-subtitle">ECHA · EFSA · Commission européenne</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Commission européenne</span>
|
||||||
|
<span class="actu-date">En cours — 2025/2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Évaluation du règlement BPR en cours depuis Q4 2025.</strong> La Commission européenne a lancé fin 2025 une évaluation complète du règlement BPR (UE 528/2012) par rapport à ses objectifs initiaux et aux besoins émergents (durabilité, simplification, résistances). Les résultats orienteront une révision potentielle du texte à moyen terme, avec des implications directes sur le Certibiocide.</p>
|
||||||
|
<a href="https://health.ec.europa.eu/biocides/regulation_en" target="_blank" rel="noopener">Commission européenne — BPR</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ECHA</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Programme de révision des substances actives — 42 % achevé.</strong> L'ECHA indique que 42 % du programme d'examen des substances actives biocides existantes est désormais terminé. Ce programme est essentiel car il conditionne les approbations européennes, et donc les AMM nationales. Les substances non encore évaluées restent sous statut transitoire.</p>
|
||||||
|
<a href="https://echa.europa.eu/fr/regulations/biocidal-products-regulation/upcoming-deadlines" target="_blank" rel="noopener">ECHA — Prochaines échéances</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">ECHA</span>
|
||||||
|
<span class="actu-date">Échéance : 31/12/2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Deadline perturbateurs endocriniens — données à soumettre avant fin 2026.</strong> Le 31 décembre 2026 est la date limite à laquelle les données manquantes sur les perturbateurs endocriniens (PE) doivent être soumises pour plusieurs substances actives biocides. Les fabricants et importateurs concernés sont en cours de constitution des dossiers. Impact potentiel sur la disponibilité de certains produits utilisés en TP 18 et 19.</p>
|
||||||
|
<a href="https://echa.europa.eu/fr/regulations/biocidal-products-regulation/upcoming-deadlines" target="_blank" rel="noopener">ECHA — Upcoming deadlines</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Commission européenne</span>
|
||||||
|
<span class="actu-date">2026 (proposition)</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Réforme Omnibus — approbation indéfinie des substances actives biocides.</strong> La Commission européenne propose de supprimer le renouvellement obligatoire tous les 10 ans des substances actives biocides approuvées sous le BPR, dès lors qu'elles ne relèvent pas des critères d'exclusion ou de substitution. Les substances éligibles passeraient automatiquement en approbation indéfinie — simplification majeure pour les industriels.</p>
|
||||||
|
<a href="https://www.knoell.com/en/news/monthly-global-biocides-regulatory-newsletter-february-2026" target="_blank" rel="noopener">Knoell — Newsletter biocides</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 8. RH & CONCURRENCE ─── -->
|
||||||
|
<section class="section" id="rh">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-emoji">👥</span>
|
||||||
|
<div>
|
||||||
|
<h2>RH & Concurrence</h2>
|
||||||
|
<div class="section-subtitle">Indeed · Glassdoor · France Travail · CS3D</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Indeed</span>
|
||||||
|
<span class="actu-date">23/06/2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>50+ offres "Technicien Certibiocide" sur Indeed en juin 2026.</strong> Au 23 juin 2026, Indeed recense plus de 50 offres d'emploi ciblant spécifiquement les techniciens titulaires du Certibiocide. Ce volume confirme que la certification est devenue un prérequis de recrutement dans le secteur 3D — un argument pédagogique fort à valoriser en formation.</p>
|
||||||
|
<a href="https://fr.indeed.com/q-technicien-certibiocide-emplois.html" target="_blank" rel="noopener">Indeed — Offres Certibiocide</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">Indeed / Glassdoor</span>
|
||||||
|
<span class="actu-date">Juin–Juillet 2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>94 offres 3D actives sur Glassdoor France, 25+ sur Indeed.</strong> Le marché de l'emploi 3D reste en tension : Glassdoor dénombre 84 offres "dératisation désinsectisation" en France, et Indeed plus de 25 offres spécifiques "dératisation 3D" (au 11/06/2026). Les postes proposent en moyenne 1 752 €/mois brut avec véhicule utilitaire fourni.</p>
|
||||||
|
<a href="https://www.glassdoor.fr/Emploi/deratisation-desinsectisation-et-emplois-SRCH_KO0,32.htm" target="_blank" rel="noopener">Glassdoor — Offres 3D</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">CS3D / France Compétences</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>CQP Maîtrise des nuisibles (RNCP niveau 3) — intègre le Certibiocide.</strong> Le CQP Technicien applicateur en maîtrise des nuisibles, développé par la CS3D et inscrit au RNCP niveau 3 (équivalent CAP), inclut le passage du Certibiocide dans un programme de 3 mois. Avec 1 200+ entreprises actives et 500 M€ de CA sectoriel, les débouchés restent nombreux pour les diplômés.</p>
|
||||||
|
<a href="https://www.francecompetences.fr/recherche/rncp/36313/" target="_blank" rel="noopener">France Compétences — RNCP 36313</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actu">
|
||||||
|
<div class="actu-header">
|
||||||
|
<span class="source-badge">France Travail</span>
|
||||||
|
<span class="actu-date">2026</span>
|
||||||
|
</div>
|
||||||
|
<p><strong>Offres en régions — exemple : Technicien 3D à Perpignan (CDI, réf. 196LSNT).</strong> France Travail recense des offres de techniciens dératisation/désinsectisation/désinfection dans toutes les régions. L'offre en Occitanie illustre la diffusion nationale des recrutements 3D. La maîtrise du Certibiocide y est systématiquement mentionnée comme un "atout fortement apprécié" ou une exigence.</p>
|
||||||
|
<a href="https://candidat.francetravail.fr/offres/recherche/detail/196LSNT" target="_blank" rel="noopener">France Travail — Offre Perpignan</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer>
|
||||||
|
<p>© 2026 NextGN Formation - Veille Certibiocide Semaine 28</p>
|
||||||
|
<p style="margin-top:10px; opacity:0.6;">Tous droits réservés - Formateurs indépendants</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const scrolled = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
|
||||||
|
document.getElementById('progressBar').style.width = scrolled + '%';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">NextGN Formation</div>
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="../../index.html">Exercices interactifs</a>
|
<a href="../../index.html">Exercices interactifs</a>
|
||||||
<a href="../../index.html#documentation">Documentation</a>
|
<a href="../../index.html#documentation">Documentation</a>
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2026 NextGN Formation - Formations Certibiocide & Techniques</p>
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
||||||
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user