Initialise le dépôt Bonpoint avec docs et prototype web.

Structure docs/, web/ (app Node existante) et mobile/ (placeholder Flutter).
This commit is contained in:
jmartin
2026-06-12 11:39:51 +02:00
commit c4ebb5b2d8
40 changed files with 5630 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#667eea">
<title>Boutique — Bons Points</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Bons Points">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/style.css?v=34">
</head>
<body>
<div class="container">
<a id="lien-retour" class="btn-retour">
<svg class="btn-retour-icone" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 18 9 12 15 6"/>
</svg>
Retour
</a>
<div id="boutique-contenu" class="cacher">
<div class="fiche-entete">
<div id="avatar-boutique" class="avatar-cercle fiche-avatar-centre"></div>
<div id="score-bandeau" class="score-bandeau"></div>
</div>
<p class="etape-label">Choisis ta récompense</p>
<div id="liste-recompenses" class="grille-recompenses"></div>
</div>
<p id="erreur" class="erreur-page cacher">Enfant introuvable</p>
</div>
<div id="modal-confirm" class="modal">
<div class="modal-contenu">
<p id="modal-texte"></p>
<div class="modal-boutons">
<button class="btn btn-principal" id="btn-confirmer">Oui !</button>
<button class="btn btn-secondaire" id="btn-annuler">Non</button>
</div>
</div>
</div>
<div id="feedback" class="feedback"></div>
<script>
const prenomUrl = window.location.pathname.split('/').pop();
const PHOTOS = {
Ariana: '/photos/ariana.png',
Pablo: '/photos/pablo.png',
'Hélia': '/photos/helia.png',
};
function slug(prenom) {
return prenom.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
let enfant = null;
let recompenses = [];
let recompenseChoisie = null;
const feedback = document.getElementById('feedback');
const modal = document.getElementById('modal-confirm');
function afficherFeedback(msg) {
feedback.textContent = msg;
feedback.classList.add('visible');
setTimeout(() => feedback.classList.remove('visible'), 2500);
}
function mettreAJourBandeau() {
const bandeau = document.getElementById('score-bandeau');
bandeau.innerHTML = `
<span class="score-bandeau-nom">${enfant.prenom}</span>
<span class="score-bandeau-pts" style="color:${enfant.couleur}">
${enfant.score} points
</span>
`;
bandeau.style.borderColor = enfant.couleur;
}
function afficherRecompenses() {
mettreAJourBandeau();
const cont = document.getElementById('liste-recompenses');
cont.innerHTML = recompenses.map((r) => {
const ok = enfant.score >= r.cout_points;
return `
<button class="carte-recompense ${ok ? '' : 'indisponible'}"
data-id="${r.id}" ${ok ? '' : 'disabled'}>
<span class="rec-icone">${r.icone}</span>
<span class="rec-libelle">${r.libelle}</span>
<span class="rec-cout">${r.cout_points} pts</span>
</button>
`;
}).join('');
cont.querySelectorAll('.carte-recompense:not(.indisponible)').forEach((btn) => {
btn.addEventListener('click', () => {
recompenseChoisie = recompenses.find(
(x) => x.id === parseInt(btn.dataset.id, 10),
);
document.getElementById('modal-texte').textContent =
`${enfant.prenom}, tu veux « ${recompenseChoisie.libelle} » ` +
`pour ${recompenseChoisie.cout_points} points ?`;
modal.classList.add('ouvert');
});
});
}
function fermerModal() {
modal.classList.remove('ouvert');
}
async function confirmerAchat() {
fermerModal();
const res = await fetch('/api/public/acheter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enfant_id: enfant.id,
recompense_id: recompenseChoisie.id,
}),
});
const data = await res.json();
if (!res.ok) {
afficherFeedback(data.erreur || 'Erreur');
return;
}
enfant = data.enfant;
afficherFeedback(
`Bravo ${data.enfant.prenom} ! ${data.recompense.icone} ` +
`${data.recompense.libelle}`,
);
afficherRecompenses();
}
async function charger() {
try {
const [resEnfant, resRecompenses] = await Promise.all([
fetch(`/api/public/enfant/${prenomUrl}`),
fetch('/api/public/recompenses'),
]);
if (!resEnfant.ok) throw new Error('introuvable');
const data = await resEnfant.json();
enfant = data.enfant;
recompenses = (await resRecompenses.json()).recompenses;
const avatar = document.getElementById('avatar-boutique');
avatar.style.borderColor = enfant.couleur;
avatar.innerHTML =
`<img src="${PHOTOS[enfant.prenom]}" alt="${enfant.prenom}">`;
mettreAJourBandeau();
document.getElementById('lien-retour').href =
`/enfant/${slug(enfant.prenom)}`;
document.title = `Boutique — ${enfant.prenom}`;
afficherRecompenses();
document.getElementById('boutique-contenu').classList.remove('cacher');
} catch {
document.getElementById('erreur').classList.remove('cacher');
}
}
document.getElementById('btn-confirmer').addEventListener('click', confirmerAchat);
document.getElementById('btn-annuler').addEventListener('click', fermerModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) fermerModal();
});
charger();
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>
+894
View File
@@ -0,0 +1,894 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 480px;
margin: 0 auto;
padding: 1.5rem 1rem 2rem;
}
h1 {
text-align: center;
color: #fff;
font-size: 1.8rem;
margin-bottom: 0.3rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.sous-titre {
text-align: center;
color: rgba(255, 255, 255, 0.85);
font-size: 0.95rem;
margin-bottom: 1.5rem;
}
.carte-enfant {
background: #fff;
border-radius: 20px;
padding: 1.2rem 1.5rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
transition: transform 0.2s;
}
.carte-enfant:hover {
transform: scale(1.02);
}
.carte-lien {
text-decoration: none;
color: inherit;
cursor: pointer;
}
.fleche-fiche {
font-size: 1.8rem;
color: #ccc;
font-weight: 300;
line-height: 1;
}
.btn-retour {
display: inline-flex;
align-items: center;
gap: 0.45rem;
background: #fff;
color: #667eea;
text-decoration: none;
font-size: 0.95rem;
font-weight: 700;
padding: 0.55rem 1.1rem 0.55rem 0.9rem;
border-radius: 24px;
margin-bottom: 1rem;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.18);
transition: transform 0.15s, box-shadow 0.15s;
}
.btn-retour-icone {
flex-shrink: 0;
display: block;
}
.btn-retour:hover {
transform: translateY(-1px);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.22);
}
.btn-retour:active {
transform: scale(0.97);
}
.fiche-entete {
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: 1.2rem;
}
.fiche-entete-compact {
display: flex;
align-items: center;
gap: 1rem;
text-align: left;
background: #fff;
border-radius: 20px;
padding: 1rem 1.2rem;
margin-bottom: 1rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
}
.fiche-entete .fiche-avatar-centre.avatar-cercle {
width: 50%;
height: auto;
aspect-ratio: 1;
margin: 0 auto 0.75rem;
border-width: 4px;
flex-shrink: 0;
}
.fiche-entete .score-bandeau {
width: 100%;
margin-top: 0;
}
.avatar-grand {
width: 96px;
height: 96px;
margin: 0 auto 0.8rem;
}
.fiche-prenom {
font-size: 1.8rem;
font-weight: 800;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.fiche-prenom-petit {
font-size: 1.3rem;
color: #333;
text-shadow: none;
}
.fiche-score {
font-size: 3.5rem;
font-weight: 800;
line-height: 1;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.fiche-section {
background: #fff;
border-radius: 20px;
padding: 1rem 1.2rem;
margin-bottom: 1rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
}
.fiche-section h3 {
font-size: 1rem;
color: #4a5568;
margin-bottom: 0.8rem;
}
.historique-vide {
color: #888;
font-size: 0.85rem;
text-align: center;
padding: 0.5rem 0;
}
.historique-libelle {
flex: 1;
padding-right: 0.5rem;
}
.historique-droite {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.1rem;
flex-shrink: 0;
}
.historique-date {
color: #aaa;
font-size: 0.75rem;
}
.fiche-section .historique-item {
flex-direction: row;
align-items: flex-start;
gap: 0.3rem;
}
.solde-badge-boutique {
font-size: 1.1rem;
font-weight: 700;
color: #667eea;
margin-top: 0.2rem;
}
.erreur-page {
text-align: center;
color: #fff;
padding: 2rem 0;
}
.avatar-cercle {
width: 58px;
height: 58px;
border-radius: 50%;
border: 3px solid;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.avatar-cercle img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.info-enfant {
flex: 1;
}
.prenom {
font-size: 1.4rem;
font-weight: 700;
}
.score {
font-size: 2.2rem;
font-weight: 800;
line-height: 1.1;
}
.score-label {
font-size: 0.85rem;
color: #888;
}
.avatar-parent-accueil {
border-color: #667eea;
flex-shrink: 0;
}
.avatar-parent-titre {
width: 2.1rem;
height: 2.1rem;
border-color: rgba(255, 255, 255, 0.85);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.pin-entete .avatar-parent-pin.avatar-cercle {
width: 50%;
height: auto;
aspect-ratio: 1;
margin: 0 auto 0.75rem;
border-width: 4px;
border-color: #667eea;
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.25);
}
.btn-parent {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
width: 100%;
margin-top: 0;
padding: 1.2rem 1.5rem;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.18);
border: 2px solid rgba(255, 255, 255, 0.55);
border-radius: 20px;
color: #fff;
text-decoration: none;
backdrop-filter: blur(4px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
transition: background 0.2s, transform 0.15s;
}
.btn-parent-texte {
flex: 1;
font-size: 1.4rem;
font-weight: 700;
line-height: 1.1;
}
.btn-parent:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.btn-parent:active {
transform: scale(0.98);
}
/* Zone parent */
body.parent-page {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.parent-container {
max-width: 480px;
margin: 0 auto;
padding: 1.2rem 1rem 2rem;
}
.parent-barre-haut {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
}
.parent-titre {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 1.3rem;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin: 0;
}
.btn-outline-blanc {
background: rgba(255, 255, 255, 0.2);
color: #fff;
border: 2px solid rgba(255, 255, 255, 0.6);
padding: 0.4rem 0.9rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-outline-blanc:hover {
background: rgba(255, 255, 255, 0.35);
}
#ecran-pin .btn-retour {
margin-bottom: 1rem;
}
.ecran-pin-carte {
background: #fff;
border-radius: 24px;
padding: 1.75rem 1.5rem 1.5rem;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}
.pin-zone {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0.6rem;
width: 100%;
}
.pin-entete {
text-align: center;
margin-bottom: 1.25rem;
}
.pin-entete h1 {
font-size: 1.4rem;
color: #667eea;
text-shadow: none;
margin: 0 0 0.35rem;
}
.pin-sous-titre {
color: #888;
font-size: 0.9rem;
margin: 0;
}
#form-pin {
flex: 1;
min-width: 0;
}
.pin-input {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 1.5rem;
text-align: center;
letter-spacing: 0.35rem;
padding: 0.8rem 0.75rem;
border: 2px solid #ddd;
border-radius: 12px;
margin-bottom: 0;
direction: ltr;
}
.pin-input:focus {
outline: none;
border-color: #667eea;
}
.btn-connexion-pin {
flex-shrink: 0;
align-self: stretch;
padding: 0.8rem 1.25rem;
font-size: 0.95rem;
margin-top: 0;
white-space: nowrap;
}
#ecran-pin .erreur {
text-align: center;
margin-top: 0.75rem;
margin-bottom: 0;
}
.pin-masque {
-webkit-text-security: disc;
}
.pin-leurre {
position: absolute;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
overflow: hidden;
}
.btn {
display: inline-block;
padding: 0.7rem 1.5rem;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.85;
}
.btn-principal {
background: #667eea;
color: #fff;
}
.btn-danger {
background: #e53e3e;
color: #fff;
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
.erreur {
color: #e53e3e;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.section {
background: #fff;
border-radius: 20px;
padding: 1rem 1.1rem;
margin-bottom: 1rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
}
.section h3 {
font-size: 0.95rem;
margin-bottom: 0.8rem;
color: #667eea;
font-weight: 700;
}
.section-enfant .selecteur-enfant {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
}
.chip-enfant {
display: flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
padding: 0.2rem;
border: none;
background: transparent;
cursor: pointer;
width: 0;
min-width: 0;
font-family: inherit;
}
.chip-vignette {
position: relative;
width: 100%;
max-width: 6.75rem;
aspect-ratio: 1;
border-radius: 50%;
border: 3px solid;
overflow: visible;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: transform 0.2s, border-width 0.2s, box-shadow 0.2s;
}
.chip-enfant.actif .chip-vignette {
border-width: 5px;
transform: scale(1.06);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.25);
}
.chip-enfant:not(.actif) .chip-vignette {
border-width: 3px;
}
.chip-vignette img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
display: block;
}
.chip-badge {
position: absolute;
right: -0.15rem;
bottom: -0.1rem;
min-width: 1.55rem;
height: 1.55rem;
padding: 0 0.25rem;
border-radius: 999px;
background: #fff;
border: 2px solid currentColor;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: 800;
line-height: 1;
font-variant-numeric: tabular-nums;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
.avatar-chip {
width: 44px;
height: 44px;
}
.chip-label {
line-height: 1.1;
font-size: 0.78rem;
}
.score-bandeau {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: #f8f9ff;
border-radius: 16px;
border-left: 4px solid;
margin-top: 0.75rem;
}
.score-bandeau-nom {
font-weight: 800;
font-size: 1.35rem;
color: #333;
line-height: 1.1;
}
.score-bandeau-pts {
font-size: 1.35rem;
font-weight: 800;
line-height: 1.1;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.grille-regles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.famille-regles {
margin-bottom: 1.1rem;
}
.famille-regles:last-child {
margin-bottom: 0;
}
.famille-titre {
font-size: 0.85rem;
font-weight: 700;
color: #fff;
background: #667eea;
margin-bottom: 0.6rem;
padding: 0.35rem 0.6rem;
border-radius: 8px;
}
.btn-regle {
padding: 0.7rem 0.5rem;
border: none;
border-radius: 12px;
font-size: 0.85rem;
cursor: pointer;
text-align: center;
line-height: 1.3;
transition: transform 0.15s;
}
.btn-regle:active {
transform: scale(0.95);
}
.btn-regle.positif {
background: #c6f6d5;
color: #22543d;
}
.btn-regle.negatif {
background: #fed7d7;
color: #742a2a;
}
.historique-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f0f0f0;
font-size: 0.85rem;
}
.historique-item:last-child {
border-bottom: none;
}
.parent-page .historique-item {
align-items: flex-start;
}
.parent-page .historique-libelle {
min-width: 0;
line-height: 1.35;
padding-right: 0.5rem;
}
.parent-page .historique-droite {
flex-direction: row;
align-items: flex-start;
gap: 0.4rem;
padding-top: 0.05rem;
flex-shrink: 0;
}
.parent-page .historique-points-col {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.1rem;
}
.parent-page .btn-annuler-mouvement {
margin-left: 0.15rem;
}
.delta-positif {
color: #38a169;
font-weight: 700;
}
.delta-negatif {
color: #e53e3e;
font-weight: 700;
}
.cacher {
display: none;
}
.feedback {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
background: #2d3748;
color: #fff;
padding: 0.8rem 1.5rem;
border-radius: 12px;
font-size: 0.95rem;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 100;
}
.feedback.visible {
opacity: 1;
}
/* Boutique */
.btn-boutique {
display: block;
text-align: center;
background: #fff;
color: #667eea;
font-weight: 700;
font-size: 1.1rem;
padding: 1rem;
border-radius: 16px;
text-decoration: none;
margin-top: 0.5rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.btn-boutique:hover {
transform: scale(1.02);
}
.etape-label {
color: #fff;
font-weight: 600;
margin-bottom: 0.8rem;
font-size: 1rem;
}
.solde-badge {
display: inline-block;
background: rgba(255, 255, 255, 0.25);
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.85rem;
margin-left: 0.3rem;
}
.grille-enfants {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.carte-cliquable {
cursor: pointer;
border: none;
width: 100%;
text-align: left;
font-family: inherit;
}
.carte-cliquable:active {
transform: scale(0.98);
}
.grille-recompenses {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
}
.carte-recompense {
background: #fff;
border: none;
border-radius: 16px;
padding: 1rem 0.6rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
transition: transform 0.15s;
font-family: inherit;
}
.carte-recompense:active {
transform: scale(0.95);
}
.carte-recompense.indisponible {
opacity: 0.4;
cursor: not-allowed;
}
.rec-icone {
font-size: 2rem;
}
.rec-libelle {
font-size: 0.8rem;
font-weight: 600;
text-align: center;
line-height: 1.2;
}
.rec-cout {
font-size: 0.85rem;
color: #667eea;
font-weight: 700;
}
.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
padding: 1rem;
}
.modal.ouvert {
display: flex;
}
.modal-contenu {
background: #fff;
border-radius: 20px;
padding: 1.5rem;
max-width: 320px;
width: 100%;
text-align: center;
}
.modal-contenu p {
font-size: 1.1rem;
margin-bottom: 1.2rem;
line-height: 1.4;
}
.modal-boutons {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.btn-secondaire {
background: #e2e8f0;
color: #4a5568;
}
.btn-annuler-mouvement {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6rem;
height: 1.6rem;
background: #fff5f5;
border: 1px solid #fc8181;
color: #e53e3e;
font-size: 0.85rem;
font-weight: 700;
line-height: 1;
padding: 0;
border-radius: 50%;
cursor: pointer;
margin-left: 0.35rem;
flex-shrink: 0;
transition: background 0.15s, transform 0.15s;
}
.btn-annuler-mouvement:hover {
background: #fed7d7;
}
.btn-annuler-mouvement:active {
transform: scale(0.92);
}
+126
View File
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#667eea">
<title>Bons Points</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Bons Points">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/style.css?v=34">
</head>
<body>
<div class="container">
<a href="/" class="btn-retour">
<svg class="btn-retour-icone" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 18 9 12 15 6"/>
</svg>
Retour
</a>
<div id="fiche" class="cacher">
<div class="fiche-entete">
<div id="avatar-grand" class="avatar-cercle fiche-avatar-centre"></div>
<div id="score-bandeau" class="score-bandeau"></div>
</div>
<div class="fiche-section">
<h3>Mon historique</h3>
<div id="historique"></div>
</div>
<a id="lien-boutique" class="btn-boutique">🛍️ Dépenser mes points</a>
</div>
<p id="erreur" class="erreur-page cacher">Enfant introuvable</p>
</div>
<script>
const PHOTOS = {
Ariana: '/photos/ariana.png',
Pablo: '/photos/pablo.png',
'Hélia': '/photos/helia.png',
};
function slug(prenom) {
return prenom.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const prenomUrl = window.location.pathname.split('/').pop();
function afficherHistorique(mouvements) {
const cont = document.getElementById('historique');
if (!mouvements.length) {
cont.innerHTML = '<p class="historique-vide">Aucun mouvement pour l\'instant</p>';
return;
}
cont.innerHTML = mouvements.map((m) => {
const date = new Date(m.cree_le + 'Z').toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
});
const cls = m.delta >= 0 ? 'delta-positif' : 'delta-negatif';
const signe = m.delta > 0 ? '+' : '';
const libelle = m.recompense || m.regle || m.note || '?';
const icone = m.icone_recompense || m.icone || '';
const type = m.recompense_id ? 'achat' : (m.delta >= 0 ? 'gain' : 'perte');
return `
<div class="historique-item historique-${type}">
<span class="historique-libelle">${icone} ${libelle}</span>
<span class="historique-droite">
<span class="${cls}">${signe}${m.delta}</span>
<span class="historique-date">${date}</span>
</span>
</div>
`;
}).join('');
}
async function charger() {
try {
const res = await fetch(`/api/public/enfant/${prenomUrl}`);
if (!res.ok) throw new Error('introuvable');
const data = await res.json();
const e = data.enfant;
document.getElementById('avatar-grand').style.borderColor = e.couleur;
document.getElementById('avatar-grand').innerHTML =
`<img src="${PHOTOS[e.prenom]}" alt="${e.prenom}">`;
const bandeau = document.getElementById('score-bandeau');
bandeau.innerHTML = `
<span class="score-bandeau-nom">${e.prenom}</span>
<span class="score-bandeau-pts" style="color:${e.couleur}">
${e.score} points
</span>
`;
bandeau.style.borderColor = e.couleur;
document.getElementById('lien-boutique').href =
`/boutique/${slug(e.prenom)}`;
document.title = `${e.prenom} — Bons Points`;
afficherHistorique(data.mouvements);
document.getElementById('fiche').classList.remove('cacher');
} catch {
document.getElementById('erreur').classList.remove('cacher');
}
}
charger();
setInterval(charger, 30000);
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#764ba2"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="108" fill="url(#bg)"/>
<text x="256" y="300" text-anchor="middle" font-size="220" fill="#fff">P</text>
</svg>

After

Width:  |  Height:  |  Size: 414 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 10.2 12 4.5l7.5 5.7V19a1 1 0 0 1-1 1h-4.5v-5.5h-5V20H5.5a1 1 0 0 1-1-1v-8.8z"/>
<path d="M12 10.8S9.5 13 9.5 14.8a2.5 2.5 0 0 0 5 0C14.5 13 12 10.8 12 10.8z" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 378 B

+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#667eea">
<title>Bons Points</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Bons Points">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/style.css?v=39">
</head>
<body>
<div class="container">
<h1>⭐ Bons Points ⭐</h1>
<p class="sous-titre">Qui est le plus sage ?</p>
<div id="liste-enfants"></div>
<a href="/parent" class="btn-parent">
<div class="avatar-cercle avatar-parent-accueil" aria-hidden="true">
<img src="/photos/parents.png" alt="">
</div>
<span class="btn-parent-texte">Espace parents</span>
</a>
</div>
<script>
const PHOTOS = {
Ariana: '/photos/ariana.png',
Pablo: '/photos/pablo.png',
'Hélia': '/photos/helia.png',
};
function slug(prenom) {
return prenom.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
async function chargerScores() {
try {
const res = await fetch('/api/public/scores');
const data = await res.json();
const conteneur = document.getElementById('liste-enfants');
conteneur.innerHTML = data.enfants.map((e) => `
<a class="carte-enfant carte-lien" href="/enfant/${slug(e.prenom)}">
<div class="avatar-cercle" style="border-color:${e.couleur}">
<img src="${PHOTOS[e.prenom]}" alt="${e.prenom}">
</div>
<div class="info-enfant">
<div class="prenom">${e.prenom}</div>
</div>
<div class="score" style="color:${e.couleur}">${e.score}</div>
<span class="fleche-fiche"></span>
</a>
`).join('');
} catch {
document.getElementById('liste-enfants').innerHTML =
'<p style="color:#fff;text-align:center">Chargement impossible</p>';
}
}
chargerScores();
setInterval(chargerScores, 30000);
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
});
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "Bons Points",
"short_name": "Bons Points",
"description": "Suivi des bons points pour Ariana, Pablo et Hélia",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#667eea",
"theme_color": "#667eea",
"lang": "fr",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+368
View File
@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#667eea">
<title>Espace Parents — Bons Points</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Bons Points">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/style.css?v=40">
</head>
<body class="parent-page">
<div class="parent-container">
<div id="ecran-pin">
<a href="/" class="btn-retour">
<svg class="btn-retour-icone" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 18 9 12 15 6"/>
</svg>
Retour
</a>
<div class="ecran-pin-carte">
<div class="pin-entete">
<div class="avatar-cercle avatar-parent-pin" aria-hidden="true">
<img src="/photos/parents.png" alt="">
</div>
<h1>Espace Parents</h1>
<p class="pin-sous-titre">Entrez votre code PIN</p>
</div>
<div class="pin-zone">
<form id="form-pin" autocomplete="off" onsubmit="return false">
<input type="text" name="decoy-user" class="pin-leurre"
tabindex="-1" autocomplete="username" aria-hidden="true">
<input type="password" name="decoy-pass" class="pin-leurre"
tabindex="-1" autocomplete="current-password" aria-hidden="true">
<input type="text" id="pin-input" class="pin-input pin-masque"
name="bonpoint-code-parent"
inputmode="numeric" pattern="[0-9]*" maxlength="8"
placeholder="••••"
autocomplete="one-time-code"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
data-lpignore="true"
data-1p-ignore="true"
data-form-type="other"
readonly>
</form>
<button class="btn btn-principal btn-connexion-pin" id="btn-connexion">
Connexion
</button>
</div>
<p id="erreur-pin" class="erreur cacher"></p>
</div>
</div>
<div id="ecran-app" class="cacher">
<div class="parent-barre-haut">
<h1 class="parent-titre">
<div class="avatar-cercle avatar-parent-titre" aria-hidden="true">
<img src="/photos/parents.png" alt="">
</div>
Espace Parents
</h1>
<button class="btn btn-outline-blanc" id="btn-deconnexion">Déconnexion</button>
</div>
<div class="section section-enfant">
<h3>Enfant sélectionné</h3>
<div class="selecteur-enfant" id="selecteur-enfant"></div>
<div id="score-bandeau" class="score-bandeau"></div>
</div>
<div class="section">
<h3>Appliquer une règle</h3>
<div id="regles-par-famille"></div>
</div>
<div class="section">
<h3>Derniers mouvements</h3>
<div id="historique"></div>
</div>
</div>
</div>
<div id="modal-annuler" class="modal">
<div class="modal-contenu">
<p id="modal-annuler-texte"></p>
<div class="modal-boutons">
<button class="btn btn-principal" id="btn-confirmer-annuler">Oui, annuler</button>
<button class="btn btn-secondaire" id="btn-fermer-annuler">Non</button>
</div>
</div>
</div>
<div id="feedback" class="feedback"></div>
<script>
const PHOTOS = {
Ariana: '/photos/ariana.png',
Pablo: '/photos/pablo.png',
'Hélia': '/photos/helia.png',
};
let enfants = [];
let regles = [];
let enfantSelectionne = null;
const ecranPin = document.getElementById('ecran-pin');
const ecranApp = document.getElementById('ecran-app');
const pinInput = document.getElementById('pin-input');
const erreurPin = document.getElementById('erreur-pin');
const feedback = document.getElementById('feedback');
const modalAnnuler = document.getElementById('modal-annuler');
let mouvementAAnnuler = null;
function afficherFeedback(msg) {
feedback.textContent = msg;
feedback.classList.add('visible');
setTimeout(() => feedback.classList.remove('visible'), 2000);
}
async function verifierSession() {
try {
const res = await fetch('/api/parent/session');
if (res.ok) {
ecranPin.classList.add('cacher');
ecranApp.classList.remove('cacher');
await chargerDonnees();
}
} catch { /* pas connecté */ }
}
async function connexion() {
erreurPin.classList.add('cacher');
const pin = pinInput.value;
const res = await fetch('/api/parent/connexion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin }),
});
if (!res.ok) {
const data = await res.json();
erreurPin.textContent = data.erreur || 'Erreur';
erreurPin.classList.remove('cacher');
return;
}
ecranPin.classList.add('cacher');
ecranApp.classList.remove('cacher');
pinInput.value = '';
await chargerDonnees();
}
async function deconnexion() {
await fetch('/api/parent/deconnexion', { method: 'POST' });
window.location.href = '/';
}
async function chargerDonnees() {
const res = await fetch('/api/parent/donnees');
const data = await res.json();
enfants = data.enfants;
regles = data.regles;
if (!enfantSelectionne && enfants.length > 0) {
enfantSelectionne = enfants[0].id;
}
afficherSelecteur();
afficherRegles();
afficherHistorique(data.mouvements);
}
function afficherSelecteur() {
const cont = document.getElementById('selecteur-enfant');
cont.innerHTML = enfants.map((e) => `
<button class="chip-enfant ${e.id === enfantSelectionne ? 'actif' : ''}"
data-id="${e.id}">
<div class="chip-vignette" style="border-color:${e.couleur}">
<img src="${PHOTOS[e.prenom]}" alt="${e.prenom}">
<span class="chip-badge" style="color:${e.couleur}">${e.score}</span>
</div>
</button>
`).join('');
cont.querySelectorAll('.chip-enfant').forEach((btn) => {
btn.addEventListener('click', () => {
enfantSelectionne = parseInt(btn.dataset.id, 10);
afficherSelecteur();
});
});
mettreAJourBandeau();
}
function mettreAJourBandeau() {
const e = enfants.find((x) => x.id === enfantSelectionne);
const bandeau = document.getElementById('score-bandeau');
if (!e) {
bandeau.innerHTML = '';
return;
}
bandeau.innerHTML = `
<span class="score-bandeau-nom">${e.prenom}</span>
<span class="score-bandeau-pts" style="color:${e.couleur}">
${e.score} points
</span>
`;
bandeau.style.borderColor = e.couleur;
}
function afficherRegles() {
const cont = document.getElementById('regles-par-famille');
const groupes = [];
let derniereFamille = null;
for (const r of regles) {
if (r.famille !== derniereFamille) {
groupes.push({ famille: r.famille, regles: [] });
derniereFamille = r.famille;
}
groupes[groupes.length - 1].regles.push(r);
}
cont.innerHTML = groupes.map((g) => `
<div class="famille-regles">
<h4 class="famille-titre">${g.famille}</h4>
<div class="grille-regles">
${g.regles.map((r) => `
<button class="btn-regle ${r.points >= 0 ? 'positif' : 'negatif'}"
data-id="${r.id}">
${r.icone} ${r.libelle}<br>
<strong>${r.points > 0 ? '+' : ''}${r.points}</strong>
</button>
`).join('')}
</div>
</div>
`).join('');
cont.querySelectorAll('.btn-regle').forEach((btn) => {
btn.addEventListener('click', () => appliquerRegle(
parseInt(btn.dataset.id, 10),
));
});
}
async function appliquerRegle(regleId) {
const res = await fetch('/api/parent/appliquer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enfant_id: enfantSelectionne,
regle_id: regleId,
}),
});
if (!res.ok) {
afficherFeedback('Erreur');
return;
}
const data = await res.json();
const idx = enfants.findIndex((e) => e.id === data.enfant.id);
if (idx >= 0) enfants[idx] = data.enfant;
afficherSelecteur();
await chargerDonnees();
afficherFeedback(`${data.enfant.prenom} : ${data.enfant.score} pts !`);
}
function afficherHistorique(mouvements) {
const cont = document.getElementById('historique');
if (!mouvements.length) {
cont.innerHTML = '<p class="historique-vide">Aucun mouvement</p>';
return;
}
cont.innerHTML = mouvements.map((m) => {
const date = new Date(m.cree_le + 'Z').toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
});
const cls = m.delta >= 0 ? 'delta-positif' : 'delta-negatif';
const signe = m.delta > 0 ? '+' : '';
const libelle = m.recompense || m.regle || m.note || '?';
const icone = m.icone_recompense || m.icone || '';
return `
<div class="historique-item">
<span class="historique-libelle">${icone} ${m.enfant}${libelle}</span>
<span class="historique-droite">
<span class="historique-points-col">
<span class="${cls}">${signe}${m.delta}</span>
<span class="historique-date">${date}</span>
</span>
<button class="btn-annuler-mouvement" data-id="${m.id}"
data-enfant="${m.enfant}" data-libelle="${libelle}"
data-delta="${m.delta}" title="Annuler ce mouvement"
aria-label="Annuler ce mouvement">✕</button>
</span>
</div>
`;
}).join('');
cont.querySelectorAll('.btn-annuler-mouvement').forEach((btn) => {
btn.addEventListener('click', () => ouvrirModalAnnuler(btn));
});
}
function ouvrirModalAnnuler(btn) {
const delta = parseInt(btn.dataset.delta, 10);
const signe = delta > 0 ? '+' : '';
mouvementAAnnuler = parseInt(btn.dataset.id, 10);
document.getElementById('modal-annuler-texte').textContent =
`Annuler « ${btn.dataset.libelle} » pour ${btn.dataset.enfant} (${signe}${delta}) ? Les points seront recalculés.`;
modalAnnuler.classList.add('ouvert');
}
function fermerModalAnnuler() {
modalAnnuler.classList.remove('ouvert');
mouvementAAnnuler = null;
}
async function confirmerAnnulerMouvement() {
if (!mouvementAAnnuler) return;
const res = await fetch('/api/parent/annuler-mouvement', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mouvement_id: mouvementAAnnuler }),
});
fermerModalAnnuler();
if (!res.ok) {
afficherFeedback('Impossible d\'annuler');
return;
}
await chargerDonnees();
afficherFeedback('Mouvement annulé');
}
document.getElementById('btn-connexion').addEventListener('click', connexion);
pinInput.addEventListener('focus', () => pinInput.removeAttribute('readonly'));
pinInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') connexion();
});
document.getElementById('btn-deconnexion').addEventListener('click', deconnexion);
document.getElementById('btn-confirmer-annuler')
.addEventListener('click', confirmerAnnulerMouvement);
document.getElementById('btn-fermer-annuler')
.addEventListener('click', fermerModalAnnuler);
modalAnnuler.addEventListener('click', (e) => {
if (e.target === modalAnnuler) fermerModalAnnuler();
});
verifierSession();
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

+250
View File
@@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Règlement des Bons Points</title>
<style>
@page { size: A4 portrait; margin: 10mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 6.8pt;
line-height: 1.2;
color: #1a1a2e;
background: #fff;
width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 6mm 8mm;
}
header {
text-align: center;
margin-bottom: 4mm;
padding-bottom: 3mm;
border-bottom: 2px solid #667eea;
}
h1 {
font-size: 14pt;
color: #667eea;
letter-spacing: 0.5px;
}
.sous-titre {
font-size: 8pt;
color: #666;
margin-top: 1mm;
}
.enfants {
display: flex;
justify-content: center;
gap: 6mm;
margin-top: 2mm;
font-size: 8pt;
font-weight: 600;
}
.enfant-tag {
display: inline-flex;
align-items: center;
gap: 1.5mm;
padding: 1mm 3mm;
border-radius: 20px;
color: #333;
background: #fff;
border: 2px solid;
font-weight: 600;
}
.enfant-tag img {
width: 6mm;
height: 6mm;
border-radius: 50%;
object-fit: cover;
}
.grille {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3mm;
}
section {
break-inside: avoid;
}
h2 {
font-size: 8pt;
padding: 1.5mm 2mm;
border-radius: 3px;
margin-bottom: 1.5mm;
color: #fff;
}
h2.maison { background: #4a90d9; }
h2.routine { background: #9b59b6; }
h2.fratrie { background: #e67e22; }
h2.respect { background: #e74c3c; }
h2.ecrans { background: #34495e; }
h2.bonus { background: #f1c40f; color: #333; }
h2.boutique { background: #27ae60; }
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 2mm;
}
td {
padding: 0.5mm 1mm;
vertical-align: top;
border-bottom: 0.3px solid #eee;
}
td.pts {
width: 8mm;
text-align: center;
font-weight: 700;
white-space: nowrap;
}
.plus { color: #27ae60; }
.moins { color: #e74c3c; }
.cout { color: #8e44ad; font-weight: 700; }
.note {
grid-column: 1 / -1;
text-align: center;
font-size: 6.5pt;
color: #888;
margin-top: 1mm;
padding-top: 2mm;
border-top: 1px dashed #ccc;
}
.boutique-section {
grid-column: 1 / -1;
}
.boutique-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 0 2mm;
}
.print-hint {
text-align: center;
font-size: 7pt;
color: #667eea;
margin-bottom: 3mm;
padding: 2mm;
background: #f0f4ff;
border-radius: 4px;
}
@media print {
body { padding: 5mm 7mm; }
.print-hint { display: none; }
}
</style>
</head>
<body>
<p class="print-hint">🖨️ Ctrl+P → Format A4, Portrait, marges minimales — pour imprimer ou enregistrer en PDF</p>
<header>
<h1>⭐ Règlement des Bons Points ⭐</h1>
<p class="sous-titre">bonpoint.ptits-pas.fr — Les points ne descendent jamais en dessous de zéro</p>
<div class="enfants">
<span class="enfant-tag" style="border-color:#E91E8C">
<img src="/photos/ariana.png" alt="Ariana"> Ariana
</span>
<span class="enfant-tag" style="border-color:#2196F3">
<img src="/photos/pablo.png" alt="Pablo"> Pablo
</span>
<span class="enfant-tag" style="border-color:#FF9800">
<img src="/photos/helia.png" alt="Hélia"> Hélia
</span>
</div>
</header>
<div class="grille">
<section>
<h2 class="maison">🏠 Maison & rangement</h2>
<table>
<tr><td>🍽️ Je mets la table</td><td class="pts plus">+1</td></tr>
<tr><td>🧹 Débarrasser la table</td><td class="pts plus">+1</td></tr>
<tr><td>🫧 Ranger le lave-vaisselle</td><td class="pts plus">+1</td></tr>
<tr><td>✨ Nettoyer la table du séjour</td><td class="pts plus">+1</td></tr>
<tr><td>🎮 Ranger la salle de jeu</td><td class="pts plus">+2</td></tr>
<tr><td>🛏️ Ranger sa chambre</td><td class="pts plus">+1</td></tr>
<tr><td>👕 Ranger ses habits propres</td><td class="pts plus">+1</td></tr>
<tr><td>🏕️ Ranger la cabane</td><td class="pts plus">+1</td></tr>
<tr><td>🚲 Ranger les vélos (abri jardin)</td><td class="pts plus">+1</td></tr>
<tr><td>👨‍👩‍👧 Ranger sur ordre des parents</td><td class="pts plus">+1</td></tr>
<tr><td>🗄️ Piquer dans les placards</td><td class="pts moins">2</td></tr>
</table>
</section>
<section>
<h2 class="routine">⏰ Matin, soir & école</h2>
<table>
<tr><td>⏰ Habits prêts + s'habiller seul<br><small>Ariana 7h20 / Pablo 8h10</small></td><td class="pts plus">+1</td></tr>
<tr><td>🌙 Se préparer pour le lit sans bagarre (&lt; 15 min)</td><td class="pts plus">+1</td></tr>
<tr><td>📚 Devoirs faits sans rappel</td><td class="pts plus">+2</td></tr>
<tr><td>🎨 Activité calme manuelle 1h</td><td class="pts plus">+2</td></tr>
<tr><td>⏰ Retard le matin (Ariana/Pablo)</td><td class="pts moins">1</td></tr>
</table>
<h2 class="bonus" style="margin-top:2mm">🌟 Bonus</h2>
<table>
<tr><td>🌟 Bon point bonus — je suis content !</td><td class="pts plus">+1</td></tr>
</table>
<h2 class="ecrans" style="margin-top:2mm">📱 Écrans</h2>
<table>
<tr><td>📺 Se lever pour la télé sans autorisation (semaine)</td><td class="pts moins">3</td></tr>
<tr><td>📱 Utiliser un téléphone sans autorisation</td><td class="pts moins">5</td></tr>
</table>
</section>
<section>
<h2 class="fratrie">👫 Fratrie & entraide</h2>
<table>
<tr><td>🤝 Aider son frère/sa sœur (sans qu'on demande)</td><td class="pts plus">+2</td></tr>
<tr><td>🎁 Partager un jouet sans bagarre</td><td class="pts plus">+1</td></tr>
<tr><td>🗣️ Dire « pardon » tout seul</td><td class="pts plus">+1</td></tr>
<tr><td>👶 Aider avec Hélia (lire, jouer calmement)</td><td class="pts plus">+1</td></tr>
<tr><td>👊 Taper, frapper</td><td class="pts moins">2</td></tr>
<tr><td>😤 Ne pas s'excuser auprès de frère/sœur</td><td class="pts moins">2</td></tr>
<tr><td>😈 Provocation volontaire de frère/sœur</td><td class="pts moins">2</td></tr>
<tr><td>🗣️ Accuser l'autre à tort</td><td class="pts moins">2</td></tr>
<tr><td>💔 Casser / abîmer un jouet (volontairement)</td><td class="pts moins">3</td></tr>
</table>
</section>
<section>
<h2 class="respect">🙏 Respect & écoute</h2>
<table>
<tr><td>🤥 Mentir à ses parents</td><td class="pts moins">2</td></tr>
<tr><td>🤬 Dire des gros mots ou insultes</td><td class="pts moins">1</td></tr>
<tr><td>🙄 Répondre mal / faire la tête</td><td class="pts moins">1</td></tr>
<tr><td>🗯️ Crier / hurler</td><td class="pts moins">1</td></tr>
<tr><td>🚪 Faire claquer une porte</td><td class="pts moins">1</td></tr>
<tr><td>👅 Tirer la langue / geste irrespectueux</td><td class="pts moins">1</td></tr>
<tr><td>👂 Ne pas écouter une consigne (plusieurs reprises)</td><td class="pts moins">2</td></tr>
<tr><td>🏃 Partir sans permission (jardin, rue)</td><td class="pts moins">3</td></tr>
</table>
</section>
<section class="boutique-section">
<h2 class="boutique">🛍️ Boutique — Dépenser ses points</h2>
<div class="boutique-grid">
<table>
<tr><td>🍬 Un bonbon</td><td class="pts cout">1 pt</td></tr>
<tr><td>📺 10 min de télé</td><td class="pts cout">1 pt</td></tr>
<tr><td>🍪 Un biscuit / cookie</td><td class="pts cout">1 pt</td></tr>
<tr><td>🎵 Choisir la musique dans la voiture</td><td class="pts cout">1 pt</td></tr>
</table>
<table>
<tr><td>🍭 Une sucette</td><td class="pts cout">2 pts</td></tr>
<tr><td>🍫 Neige</td><td class="pts cout">3 pts</td></tr>
<tr><td>🎮 10 min de jeu vidéo</td><td class="pts cout">3 pts</td></tr>
<tr><td>🍕 Choisir le menu du repas</td><td class="pts cout">5 pts</td></tr>
<tr><td>🍦 Une glace</td><td class="pts cout">5 pts</td></tr>
</table>
<table>
<tr><td>📓 Cahier de dessin neuf</td><td class="pts cout">10 pts</td></tr>
<tr><td>🧸 Jouet pas cher (&lt; 10 €)</td><td class="pts cout">50 pts</td></tr>
</table>
</div>
</section>
<p class="note">
🌐 Voir ses points : bonpoint.ptits-pas.fr &nbsp;|&nbsp;
👨‍👩‍👧 Parents : bonpoint.ptits-pas.fr/parent
</p>
</div>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
const CACHE = 'bonpoint-v1';
const PRECACHE = [
'/',
'/css/style.css',
'/manifest.json',
'/icons/icon-192.png',
'/icons/icon-512.png',
'/icons/icon.svg',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()),
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(
keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)),
))
.then(() => self.clients.claim()),
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (url.pathname.startsWith('/api/')) return;
event.respondWith(
caches.match(request).then((cached) => {
const fetchPromise = fetch(request).then((response) => {
if (response.ok && url.origin === self.location.origin) {
const clone = response.clone();
caches.open(CACHE).then((cache) => cache.put(request, clone));
}
return response;
}).catch(() => cached);
return cached || fetchPromise;
}),
);
});