design(web): refonte des pages aide / statistiques / réglages

Typographie plus lisible (16.5px, interligne 1.7), cartes arrondies avec ombres douces, degradés teal, en-tête avec logo et slogan, onglets de navigation actifs, emojis dans toutes les sections, étapes numérotées sur la page d'aide, cartes de stats et graphique à barres sur la page statistiques, formulaire organisé en sections avec rappels santé stylisés, page de confirmation festive, mode sombre complet.
This commit is contained in:
2026-08-14 22:26:57 -04:00
parent 28860eaed1
commit 162d574ff4
2 changed files with 344 additions and 219 deletions
+340 -215
View File
@@ -143,11 +143,41 @@ proc t(lang: Lang, fr, en: string): string =
# Gabarit de page # Gabarit de page
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
proc pageShell(lang: Lang, title, body: string): string = proc statCard(emoji, label, value, unit: string): string =
let nav = "<nav><a href=\"/help\">" & t(lang, "Aide", "Help") & ## Carte de statistique (emoji + libellé + grande valeur).
"</a> <a href=\"/stats\">" & t(lang, "Statistiques", "Statistics") & "<div class='statcard'><div class='statemoji'>" & emoji & "</div>" &
"</a> <a href=\"/settings\">" & t(lang, "Réglages", "Settings") & "<div class='statlabel'>" & label & "</div>" &
"</a></nav>" "<div class='statvalue'>" & value &
(if unit.len > 0: " <span class='statunit'>" & unit & "</span>" else: "") & "</div></div>"
proc numField(emoji, label, name: string, value, minVal: int): string =
## Champ numérique libellé du formulaire de réglages.
"<label class='field'><span class='flabel'>" & emoji & " " & label & "</span>" &
"<input type='number' name='" & name & "' min='" & $minVal & "' value='" & $value & "'></label>"
proc checkRow(name: string, checked: bool, emoji, label: string): string =
## Ligne de case à cocher du formulaire de réglages.
"<label class='checkrow'><input type='checkbox' class='chk' name='" & name & "'" &
(if checked: " checked" else: "") & "><span>" & emoji & " " & label & "</span></label>"
proc pageShell(lang: Lang, title, body, active: string): string =
## Gabarit commun : en-tête (logo + slogan), onglets de navigation, corps et
## pied de page. Design : dégradés doux, cartes arrondies, mode sombre.
let nav = "<nav class='tabs'>" &
"<a class='tab" & (if active == "help": " active" else: "") & "' href='/help'>📖 " & t(lang, "Aide", "Help") & "</a>" &
"<a class='tab" & (if active == "stats": " active" else: "") & "' href='/stats'>📊 " & t(lang, "Statistiques", "Statistics") & "</a>" &
"<a class='tab" & (if active == "settings": " active" else: "") & "' href='/settings'>⚙️ " & t(lang, "Réglages", "Settings") & "</a>" &
"</nav>"
let header = "<header class='hero'>" &
"<div class='logo'>🍅</div>" &
"<div><h1>NimPulse</h1><p class='tagline'>" &
t(lang,
"Le minuteur Pomodoro qui prend soin de vous",
"The Pomodoro timer that takes care of you") &
"</p></div></header>"
let footer = "<footer><p>🔒 " & t(lang,
"Pages servies localement par NimPulse — aucune donnée ne quitte votre ordinateur.",
"Pages served locally by NimPulse — no data leaves your computer.") & "</p></footer>"
result = """<!DOCTYPE html> result = """<!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <head>
@@ -156,37 +186,111 @@ proc pageShell(lang: Lang, title, body: string): string =
<title>NimPulse — """ & title & """</title> <title>NimPulse — """ & title & """</title>
<style> <style>
:root{color-scheme:light dark} :root{color-scheme:light dark}
body{font-family:system-ui,'Segoe UI',sans-serif;max-width:800px;margin:2rem auto;padding:0 1rem;line-height:1.65;color:#1f2937;background:#fff} *{box-sizing:border-box}
body{
margin:0;font-family:'Segoe UI',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;
font-size:16.5px;line-height:1.7;color:#1e293b;min-height:100vh;
background:linear-gradient(165deg,#ecfeff 0%,#f0fdfa 30%,#f8fafc 70%,#fefce8 100%);
}
.wrap{max-width:900px;margin:0 auto;padding:1.6rem 1.3rem 3rem}
.hero{display:flex;align-items:center;gap:1rem;padding:.3rem .1rem 1.1rem}
.logo{font-size:2.7rem;line-height:1;filter:drop-shadow(0 4px 8px rgba(13,148,136,.4))}
.hero h1{margin:0;font-size:2.1rem;letter-spacing:.3px;background:linear-gradient(90deg,#0f766e,#14b8a6);-webkit-background-clip:text;background-clip:text;color:transparent}
.tagline{margin:.15rem 0 0;color:#64748b;font-size:1rem}
.tabs{display:flex;gap:.55rem;flex-wrap:wrap;margin:0 0 1.5rem}
.tab{display:inline-flex;align-items:center;gap:.45rem;padding:.55rem 1.15rem;border-radius:999px;text-decoration:none;font-weight:600;font-size:.98rem;color:#0f766e;background:rgba(255,255,255,.85);border:1px solid #99f6e4;box-shadow:0 2px 8px rgba(15,118,110,.08);transition:transform .15s ease, box-shadow .15s ease}
.tab:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(15,118,110,.18)}
.tab.active{background:linear-gradient(90deg,#0f766e,#14b8a6);color:#fff;border-color:transparent}
.card{background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:18px;padding:1.35rem 1.55rem;margin:1.1rem 0;box-shadow:0 6px 18px rgba(15,118,110,.07)}
h2{margin:2.3rem 0 .9rem;color:#0f766e;font-size:1.3rem;letter-spacing:.2px}
.card h2{margin:.1rem 0 .5rem}
p{margin:.55rem 0}
.lead{font-size:1.12rem;color:#334155}
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:1rem;margin:1rem 0}
.step{display:flex;gap:.9rem;background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:16px;padding:1.1rem 1.2rem;box-shadow:0 4px 12px rgba(15,118,110,.06)}
.step .num{flex:0 0 auto;width:2.1rem;height:2.1rem;border-radius:50%;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:1.05rem}
.steps p{margin:.2rem 0 0;color:#64748b;font-size:.95rem}
ul.clean{list-style:none;margin:.2rem 0;padding:0}
ul.clean li{padding:.45rem 0}
table{width:100%;border-collapse:collapse;margin:.7rem 0;background:rgba(255,255,255,.7);border-radius:12px;overflow:hidden}
th{background:#ccfbf1;color:#134e4a;text-align:left;padding:.55rem .8rem;font-size:.93rem}
td{padding:.5rem .8rem;border-top:1px solid #f1f5f9}
code{background:#eef2f7;border-radius:6px;padding:.08rem .4rem;font-family:'Cascadia Mono',Consolas,monospace;font-size:.92em}
.kbd{display:inline-block;background:#f1f5f9;border:1px solid #cbd5e1;border-bottom-width:3px;border-radius:7px;padding:.08rem .55rem;font-family:'Cascadia Mono',Consolas,monospace;font-size:.9em;font-weight:600}
.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin:1.1rem 0}
.statcard{background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:18px;padding:1.15rem 1.3rem;text-align:center;box-shadow:0 6px 18px rgba(15,118,110,.07)}
.statemoji{font-size:1.7rem;margin-bottom:.25rem}
.statlabel{color:#64748b;font-size:.92rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
.statvalue{font-size:2rem;font-weight:800;color:#0f766e;margin-top:.15rem;line-height:1.2}
.statunit{font-size:1rem;font-weight:600;color:#94a3b8}
.big-number{font-size:1.5rem;font-weight:800;color:#0f766e;margin:.3rem 0 .5rem}
.progress{background:#e2e8f0;border-radius:999px;height:16px;overflow:hidden;margin:.6rem 0}
.progress>div{background:linear-gradient(90deg,#0d9488,#22c55e);height:100%;border-radius:999px;transition:width .4s ease}
.goalok{color:#059669;font-weight:700}
.hint{color:#94a3b8;font-size:.92rem;margin:.1rem 0 .7rem}
.chart{display:flex;align-items:flex-end;gap:.7rem;background:rgba(255,255,255,.85);border:1px solid #e2e8f0;border-radius:16px;padding:1.4rem 1.2rem .9rem;overflow-x:auto;margin:.8rem 0}
.barcol{flex:1 1 0;min-width:52px;display:flex;flex-direction:column;align-items:center;gap:.35rem}
.barval{font-size:.9rem;font-weight:700;color:#0f766e;min-height:1.2rem}
.barfill{width:70%;max-width:64px;background:linear-gradient(180deg,#2dd4bf,#0d9488);border-radius:8px 8px 4px 4px;min-height:6px;transition:height .3s ease}
.barlabel{font-size:.85rem;color:#64748b;font-weight:600}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:.9rem 1.4rem}
@media (max-width:640px){.grid2{grid-template-columns:1fr}}
.field{display:block;margin:.2rem 0}
.flabel{display:block;font-weight:600;font-size:.95rem;color:#334155;margin-bottom:.3rem}
input[type=number],select{width:100%;padding:.55rem .7rem;border:1.5px solid #cbd5e1;border-radius:10px;font-size:1rem;font-family:inherit;color:#1e293b;background:#fff;transition:border-color .15s ease, box-shadow .15s ease}
input[type=number]:focus,select:focus{outline:none;border-color:#14b8a6;box-shadow:0 0 0 3px rgba(20,184,166,.18)}
.checkrow{display:flex;align-items:center;gap:.65rem;padding:.5rem .2rem;font-weight:600;font-size:.98rem;cursor:pointer}
.chk{width:1.25rem;height:1.25rem;accent-color:#0d9488;cursor:pointer}
.btn{display:inline-flex;align-items:center;gap:.45rem;background:linear-gradient(90deg,#0f766e,#14b8a6);color:#fff;border:0;border-radius:12px;padding:.6rem 1.25rem;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;text-decoration:none;box-shadow:0 4px 12px rgba(15,118,110,.25);transition:transform .15s ease, filter .15s ease}
.btn:hover{transform:translateY(-2px);filter:brightness(1.06)}
.btn.big{padding:.8rem 1.8rem;font-size:1.08rem}
.btn.ghost{background:#fff;color:#0f766e;border:1.5px dashed #5eead4;box-shadow:none}
.savezone{text-align:center;margin-top:1.6rem}
.empty{padding:1rem;text-align:center;color:#94a3b8;border:1.5px dashed #cbd5e1;border-radius:12px}
.remlist{display:flex;flex-direction:column;gap:.5rem}
.remrow{display:flex;align-items:center;gap:.7rem;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:.65rem .9rem;box-shadow:0 2px 8px rgba(15,118,110,.05)}
.remrow.off{opacity:.62}
.remrow.off .remtext{text-decoration:line-through}
.remtext{flex:1;font-weight:500}
.mini{border:1px solid #cbd5e1;background:#f8fafc;border-radius:9px;padding:.3rem .6rem;font-size:.88rem;font-weight:600;cursor:pointer;color:#334155;font-family:inherit}
.mini:hover{background:#f0fdfa;border-color:#5eead4}
.mini.danger{color:#b91c1c}
.mini.danger:hover{background:#fef2f2;border-color:#fca5a5}
.saved{text-align:center;padding:2.5rem 1.5rem}
.savedemoji{font-size:3rem;margin-bottom:.4rem}
footer{margin-top:2.8rem;padding-top:1.2rem;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:.9rem;text-align:center}
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark){
body{background:#111827;color:#e5e7eb} body{color:#e2e8f0;background:linear-gradient(165deg,#042f2e 0%,#0f172a 40%,#111827 80%,#1e1b4b 100%)}
h1{color:#5eead4} h2{color:#99f6e4;border-color:#374151} .card,.statcard,.step,.remrow,.chart{background:#0f172a;border-color:#1e293b}
code,pre{background:#1f2937} .card{background:#1f2937;border-color:#374151} .tagline,.hint,.statlabel,.statunit,.barlabel,footer,.flabel{color:#94a3b8}
th,td{border-color:#374151} .bar{background:#374151} .muted{color:#9ca3af} .lead,.steps p{color:#94a3b8}
input,select,button{color-scheme:dark} .hero h1{background:linear-gradient(90deg,#2dd4bf,#5eead4);-webkit-background-clip:text;background-clip:text}
h2{color:#2dd4bf}
.statvalue{color:#5eead4}
.barval{color:#5eead4}
input[type=number],select{background:#0b1220;color:#e2e8f0;border-color:#334155}
.tab{background:#0f172a;color:#5eead4;border-color:#164e63}
.tab.active{background:linear-gradient(90deg,#0d9488,#14b8a6);color:#052e2b}
th{background:#134e4a;color:#99f6e4}
td{border-top-color:#1e293b}
table{background:transparent}
code{background:#1e293b}
.kbd{background:#1e293b;border-color:#475569}
.mini{background:#1e293b;border-color:#334155;color:#cbd5e1}
.btn.ghost{background:transparent}
.progress{background:#1e293b}
.empty{color:#94a3b8;border-color:#334155}
.remrow.off{opacity:.55}
} }
h1{color:#0f766e} h2{margin-top:2rem;color:#115e59;border-bottom:1px solid #e5e7eb;padding-bottom:.3rem}
code,pre{background:#f3f4f6;padding:.1rem .35rem;border-radius:4px;font-size:.92em}
table{border-collapse:collapse;width:100%;margin:.6rem 0}
th,td{border:1px solid #e5e7eb;padding:.4rem .6rem;text-align:left}
nav{display:flex;gap:1.2rem;margin-bottom:1.6rem;padding:.6rem .8rem;border-radius:8px;background:#f0fdfa;border:1px solid #99f6e4}
nav a{color:#0f766e;text-decoration:none;font-weight:600}
.card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:1rem 1.2rem;margin:1rem 0}
.bar{background:#e5e7eb;border-radius:6px;height:14px;overflow:hidden;min-width:30px;display:inline-block;vertical-align:middle}
.bar>div{background:#14b8a6;height:100%}
.muted{color:#6b7280;font-size:.92em}
.btn{display:inline-block;background:#0f766e;color:#fff;padding:.45rem .95rem;border-radius:6px;text-decoration:none;border:0;font-size:.95rem;cursor:pointer}
.btn:hover{background:#115e59}
label{display:block;margin:.55rem 0 .15rem;font-weight:600}
input[type=text],input[type=number],select{width:100%;max-width:320px;padding:.35rem .5rem;border:1px solid #d1d5db;border-radius:6px}
input[type=checkbox]{width:auto}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 1.5rem}
@media (max-width:640px){.grid{grid-template-columns:1fr}}
.kbd{background:#f3f4f6;border:1px solid #d1d5db;border-bottom-width:2px;border-radius:4px;padding:.05rem .4rem;font-family:ui-monospace,monospace;font-size:.9em}
.ok{color:#047857;font-weight:700}
</style> </style>
</head> </head>
<body> <body>
""" & nav & body & "</body></html>" <div class="wrap">
""" & header & nav & body & footer & """
</div>
</body>
</html>"""
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Page d'aide # Page d'aide
@@ -195,116 +299,114 @@ input[type=checkbox]{width:auto}
proc renderHelp*(cfg: Config): string = proc renderHelp*(cfg: Config): string =
let lang = resolveLang(cfg) let lang = resolveLang(cfg)
var b = "" var b = ""
b.add "<h1>" & t(lang,
"Aide — Comment fonctionne NimPulse",
"Help — How NimPulse works") & "</h1>"
b.add "<div class=\"card\"><h2>" & t(lang, "Le principe", "The concept") & "</h2><p>" & b.add "<div class='card lead'><p>🍅 " & t(lang,
t(lang, "NimPulse alterne des périodes de <b>travail concentré</b> et des <b>pauses</b>, et profite des pauses pour vous rappeler de prendre soin de vous.",
"NimPulse est un minuteur Pomodoro qui vit dans la zone de notification (tray). " & "NimPulse alternates <b>focused work</b> sessions with <b>breaks</b>, and uses breaks to remind you to take care of yourself.") & "</p></div>"
"Il alterne des périodes de travail concentré et des pauses, et profite des pauses " &
"pour vous rappeler de prendre soin de votre santé (marcher, boire de l'eau, vous étirer…).",
"NimPulse is a Pomodoro timer living in the system tray. It alternates focused work " &
"sessions with breaks, and uses breaks to remind you to take care of your health " &
"(walk, drink water, stretch…).") & "</p></div>"
b.add "<h2>" & t(lang, "Premiers pas", "Getting started") & "</h2><ul>" & b.add "<h2>🧭 " & t(lang, "Comment ça marche", "How it works") & "</h2>"
t(lang, b.add "<div class='steps'>"
"<li>Au premier lancement, un assistant vous guide (langue, profil, rappels santé).</li>" & b.add "<div class='step'><div class='num'>1</div><div><b>💼 " & t(lang, "Travaillez", "Work") & "</b><p>" & t(lang,
"<li>L'application démarre minimisée : son icône colorée se trouve dans la zone de notification.</li>" & "Lancez une période de travail concentré — 25 minutes par défaut.",
"<li>Un clic droit sur l'icône ouvre le menu ; un double-clic démarre ou met en pause.</li>" & "Start a focused work session — 25 minutes by default.") & "</p></div></div>"
"<li>Le raccourci global <span class=\"kbd\">Ctrl+Alt+P</span> démarre/met en pause sans toucher la souris.</li>", b.add "<div class='step'><div class='num'>2</div><div><b>☕ " & t(lang, "Pausez", "Break") & "</b><p>" & t(lang,
"<li>On first launch, a wizard guides you (language, profile, health reminders).</li>" & "Reposez-vous pendant une pause courte, ou longue après plusieurs cycles.",
"<li>The app starts minimized: its colored icon lives in the notification area.</li>" & "Rest during a short break, or a long one after several cycles.") & "</p></div></div>"
"<li>Right-click the icon for the menu; double-click to start or pause.</li>" & b.add "<div class='step'><div class='num'>3</div><div><b>💪 " & t(lang, "Bougez", "Move") & "</b><p>" & t(lang,
"<li>The global shortcut <span class=\"kbd\">Ctrl+Alt+P</span> starts/pauses without touching the mouse.</li>") & "Pendant la pause, NimPulse vous rappelle de marcher, boire ou vous étirer.",
"</ul>" "During the break, NimPulse reminds you to walk, drink or stretch.") & "</p></div></div>"
b.add "</div>"
b.add "<h2>" & t(lang, "Les périodes", "The periods") & "</h2><p>" & b.add "<h2>🚀 " & t(lang, "Premiers pas", "Getting started") & "</h2>"
t(lang, b.add "<div class='card'><ul class='clean'>"
"Un cycle classique : travail (25 min), pause courte (5 min), et après plusieurs cycles, " & b.add "<li>🧙 " & t(lang, "Au premier lancement, un assistant vous guide : langue, profil, rappels santé.",
"une pause longue (15 min). Le nombre de cycles avant la pause longue est configurable, " & "On first launch, a wizard guides you: language, profile, health reminders.") & "</li>"
"ainsi que les durées via les profils :", b.add "<li>🎨 " & t(lang, "L'application démarre minimisée : son icône colorée vit dans la zone de notification.",
"A classic cycle: work (25 min), short break (5 min), and after several cycles a long " & "The app starts minimized: its colored icon lives in the notification area.") & "</li>"
"break (15 min). The number of cycles before the long break is configurable, as are the " & b.add "<li>🖱️ " & t(lang, "Clic droit sur l'icône = menu · double-clic = démarrer / pause.",
"durations via the profiles:") & "</p><table><tr><th>" & "Right-click the icon = menu · double-click = start / pause.") & "</li>"
t(lang, "Profil", "Profile") & "</th><th>" & t(lang, "Travail", "Work") & "</th><th>" & b.add "<li>⌨️ " & t(lang, "Le raccourci global", "The global shortcut") &
t(lang, "Pause courte", "Short break") & "</th><th>" & t(lang, "Pause longue", "Long break") & "</th><th>" & " <span class='kbd'>Ctrl+Alt+P</span> " &
t(lang, "Cycles", "Cycles") & "</th></tr>" & t(lang, "démarre ou met en pause sans toucher la souris.",
"<tr><td>" & t(lang, "Classique", "Classic") & "</td><td>25 min</td><td>5 min</td><td>15 min</td><td>4</td></tr>" & "starts or pauses without touching the mouse.") & "</li>"
"<tr><td>Deep Work</td><td>50 min</td><td>10 min</td><td>20 min</td><td>4</td></tr>" & b.add "</ul></div>"
"<tr><td>Sprint</td><td>15 min</td><td>3 min</td><td>10 min</td><td>6</td></tr></table>" &
"<p>" & t(lang, b.add "<h2>⏱️ " & t(lang, "Les périodes", "The periods") & "</h2>"
b.add "<div class='card'><p>" & t(lang,
"Un cycle classique : travail (25 min), pause courte (5 min) puis, après plusieurs cycles, une pause longue (15 min). Tout est configurable via les profils :",
"A classic cycle: work (25 min), short break (5 min) then, after several cycles, a long break (15 min). Everything is configurable via the profiles:") & "</p>"
b.add "<table><tr><th>👤 " & t(lang, "Profil", "Profile") & "</th><th>💼 " & t(lang, "Travail", "Work") &
"</th><th>☕ " & t(lang, "Pause courte", "Short break") & "</th><th>🛋️ " & t(lang, "Pause longue", "Long break") &
"</th><th>🔁 " & t(lang, "Cycles", "Cycles") & "</th></tr>"
b.add "<tr><td>" & t(lang, "Classique", "Classic") & "</td><td>25 min</td><td>5 min</td><td>15 min</td><td>4</td></tr>"
b.add "<tr><td>Deep Work</td><td>50 min</td><td>10 min</td><td>20 min</td><td>4</td></tr>"
b.add "<tr><td>Sprint</td><td>15 min</td><td>3 min</td><td>10 min</td><td>6</td></tr></table>"
b.add "<p class='hint'>" & t(lang,
"Le profil actif se choisit dans le menu de l'icône (« Profils »).", "Le profil actif se choisit dans le menu de l'icône (« Profils »).",
"The active profile is chosen from the icon menu (“Profiles”).") & "</p>" "The active profile is chosen from the icon menu (“Profiles”).") & "</p></div>"
b.add "<h2>" & t(lang, "Pendant une pause", "During a break") & "</h2><ul>" & b.add "<h2>☕ " & t(lang, "Pendant une pause", "During a break") & "</h2>"
t(lang, b.add "<div class='card'><ul class='clean'>"
"<li>« Sauter la pause » passe directement au travail suivant.</li>" & b.add "<li>⏭️ " & t(lang, "« Sauter la pause » passe directement au travail suivant.",
"<li>« Prolonger la pause » ajoute 5 minutes.</li>" & "“Skip break” jumps straight to the next work session.") & "</li>"
"<li>Les rappels santé défilent dans les notifications pendant les pauses, sans répéter deux fois le même.</li>", b.add "<li>➕ " & t(lang, "« Prolonger la pause » ajoute 5 minutes.",
"<li>“Skip break” jumps straight to the next work session.</li>" & "“Extend break” adds 5 minutes.") & "</li>"
"<li>“Extend break” adds 5 minutes.</li>" & b.add "<li>🔔 " & t(lang, "Les rappels santé défilent dans les notifications, sans répéter deux fois le même.",
"<li>Health reminders rotate through notifications during breaks, never repeating the same one twice.</li>") & "Health reminders rotate through notifications, never repeating the same one twice.") & "</li>"
"</ul>" b.add "</ul></div>"
b.add "<h2>" & t(lang, "Rappels santé", "Health reminders") & "</h2><p>" & b.add "<h2>💪 " & t(lang, "Rappels santé", "Health reminders") & "</h2>"
t(lang, b.add "<div class='card'><p>" & t(lang,
"Les rappels sont entièrement modifiables depuis la page Réglages : ajoutez, modifiez, " & "Entièrement modifiables depuis la page ⚙️ Réglages : ajoutez, modifiez, supprimez ou désactivez vos propres activités. Seuls les rappels activés sont affichés.",
"supprimez ou désactivez vos propres activités. Seuls les rappels activés sont affichés.", "Fully editable from the ⚙️ Settings page: add, edit, remove or disable your own activities. Only enabled reminders are shown.") & "</p></div>"
"Reminders are fully editable from the Settings page: add, edit, remove or disable your " &
"own activities. Only enabled reminders are shown.") & "</p>"
b.add "<h2>" & t(lang, "Statistiques et objectif", "Statistics and goal") & "</h2><p>" & b.add "<h2>📊 " & t(lang, "Statistiques et objectif", "Statistics and goal") & "</h2>"
t(lang, b.add "<div class='card'><p>" & t(lang,
"La page Statistiques affiche vos pomodoros du jour, de la semaine, votre série de jours " & "La page 📊 Statistiques affiche vos pomodoros du jour, de la semaine, votre série de jours consécutifs et le total. Définissez un objectif journalier dans les réglages : une notification vous prévient quand il est atteint 🎉. Le menu de l'icône permet aussi d'exporter l'historique en CSV.",
"consécutifs et le total. Vous pouvez définir un objectif journalier dans les réglages : " & "The 📊 Statistics page shows your pomodoros for today, this week, your streak of consecutive days and the total. Set a daily goal in settings: a notification tells you when it is reached 🎉. The icon menu also exports the history as CSV.") & "</p></div>"
"une notification vous prévient quand il est atteint. Le menu de l'icône permet aussi " &
"d'exporter l'historique en CSV.",
"The Statistics page shows your pomodoros for today, this week, your streak of consecutive " &
"days and the total. You can set a daily goal in settings: a notification tells you when " &
"it is reached. The icon menu also lets you export the history as CSV.") & "</p>"
b.add "<h2>" & t(lang, "Réglages", "Settings") & "</h2><ul>" & b.add "<h2>⚙️ " & t(lang, "Réglages", "Settings") & "</h2>"
t(lang, b.add "<div class='card'><ul class='clean'>"
"<li><b>Durées</b> : valeurs de base du minuteur (si aucun profil ne correspond).</li>" & b.add "<li>⏱️ <b>" & t(lang, "Durées", "Durations") & "</b> — " &
"<li><b>Objectif journalier</b> : nombre de pomodoros par jour (0 = aucun).</li>" & t(lang, "valeurs de base du minuteur (si aucun profil ne correspond).", "base timer values (when no profile matches).") & "</li>"
"<li><b>Son</b> : joue un son en fin de période.</li>" & b.add "<li>🎯 <b>" & t(lang, "Objectif journalier", "Daily goal") & "</b> — " &
"<li><b>Ne pas déranger</b> : suspend les rappels santé (le minuteur continue).</li>" & t(lang, "pomodoros par jour (0 = aucun).", "pomodoros per day (0 = none).") & "</li>"
"<li><b>Démarrage avec Windows</b> : lance NimPulse à l'ouverture de session.</li>" & b.add "<li>🔊 <b>" & t(lang, "Son", "Sound") & "</b> — " &
"<li><b>Langue</b> : automatique, français ou anglais.</li>" & t(lang, "joué en fin de période.", "played at the end of a period.") & "</li>"
"<li><b>Fenêtre flottante</b> : petit panneau toujours au-dessus affichant la phase et le temps restant.</li>", b.add "<li>🌙 <b>" & t(lang, "Ne pas déranger", "Do not disturb") & "</b> — " &
"<li><b>Durations</b>: base timer values (when no profile matches).</li>" & t(lang, "suspend les rappels santé (le minuteur continue).", "suspends health reminders (the timer keeps running).") & "</li>"
"<li><b>Daily goal</b>: pomodoros per day (0 = none).</li>" & b.add "<li>🚀 <b>" & t(lang, "Démarrage avec Windows", "Start with Windows") & "</b> — " &
"<li><b>Sound</b>: plays a sound at the end of a period.</li>" & t(lang, "lance NimPulse à l'ouverture de session.", "launches NimPulse at logon.") & "</li>"
"<li><b>Do not disturb</b>: suspends health reminders (the timer keeps running).</li>" & b.add "<li>🌐 <b>" & t(lang, "Langue", "Language") & "</b> — " &
"<li><b>Start with Windows</b>: launches NimPulse at logon.</li>" & t(lang, "automatique, français ou anglais.", "automatic, French or English.") & "</li>"
"<li><b>Language</b>: automatic, French or English.</li>" & b.add "<li>🖥️ <b>" & t(lang, "Fenêtre flottante", "Floating window") & "</b> — " &
"<li><b>Floating window</b>: small always-on-top panel showing the phase and remaining time.</li>") & t(lang, "petit panneau toujours au-dessus avec la phase et le temps restant.",
"</ul>" "small always-on-top panel with the phase and remaining time.") & "</li>"
b.add "</ul></div>"
b.add "<h2>" & t(lang, "Astuces", "Tips") & "</h2><ul>" & b.add "<h2>💡 " & t(lang, "Astuces", "Tips") & "</h2>"
t(lang, b.add "<div class='card'><ul class='clean'>"
"<li>En <b>mode portable</b> (fichier vide <code>portable</code> à côté de l'exécutable), " & b.add "<li>🧳 " & t(lang,
"la configuration et les statistiques sont stockées au même endroit que l'application — pratique sur clé USB.</li>" & "En <b>mode portable</b> (fichier vide <code>portable</code> à côté de l'exécutable), configuration et statistiques suivent l'application — pratique sur clé USB.",
"<li>Lancer NimPulse une seconde fois ne crée pas de doublon (mono-instance).</li>" & "In <b>portable mode</b> (empty <code>portable</code> file next to the executable), configuration and statistics follow the app — handy on USB sticks.") & "</li>"
"<li>Ces pages web (aide, statistiques, réglages) sont servies localement par l'application : " & b.add "<li>🪞 " & t(lang,
"aucune donnée ne quitte votre ordinateur.</li>", "Lancer NimPulse une seconde fois ne crée pas de doublon (mono-instance).",
"<li>In <b>portable mode</b> (empty file <code>portable</code> next to the executable), " & "Launching NimPulse a second time does not create a duplicate (single instance).") & "</li>"
"configuration and statistics are stored next to the app — handy on USB sticks.</li>" & b.add "<li>🔒 " & t(lang,
"<li>Launching NimPulse a second time does not create a duplicate (single instance).</li>" & "Ces pages sont servies localement par l'application : aucune donnée ne quitte votre ordinateur.",
"<li>These web pages (help, statistics, settings) are served locally by the app: " & "These pages are served locally by the app: no data leaves your computer.") & "</li>"
"no data leaves your computer.</li>") & b.add "</ul></div>"
"</ul>"
b.add "<h2>" & t(lang, "Raccourcis", "Shortcuts") & "</h2><table>" & b.add "<h2>⌨️ " & t(lang, "Raccourcis", "Shortcuts") & "</h2>"
"<tr><th>" & t(lang, "Action", "Action") & "</th><th>" & t(lang, "Raccourci", "Shortcut") & "</th></tr>" & b.add "<div class='card'><table>"
"<tr><td>" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td><span class=\"kbd\">Ctrl+Alt+P</span></td></tr>" & b.add "<tr><th>" & t(lang, "Action", "Action") & "</th><th>" & t(lang, "Raccourci", "Shortcut") & "</th></tr>"
"<tr><td>" & t(lang, "Menu contextuel", "Context menu") & "</td><td>" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "</td></tr>" & b.add "<tr><td>▶️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td><span class='kbd'>Ctrl+Alt+P</span></td></tr>"
"<tr><td>" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td>" & t(lang, "Double-clic", "Double-click") & "</td></tr>" & b.add "<tr><td>📋 " & t(lang, "Menu contextuel", "Context menu") & "</td><td>" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "</td></tr>"
"</table>" b.add "<tr><td>🖱️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td>" & t(lang, "Double-clic", "Double-click") & "</td></tr>"
b.add "</table></div>"
pageShell(lang, t(lang, "Aide", "Help"), b, "help")
pageShell(lang, t(lang, "Aide", "Help"), b)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Page statistiques # Page statistiques
@@ -324,40 +426,42 @@ proc renderStats*(cfg: Config): string =
if n > maxv: maxv = n if n > maxv: maxv = n
var b = "" var b = ""
b.add "<h1>" & t(lang, "Statistiques", "Statistics") & "</h1>" b.add "<div class='statgrid'>"
b.add statCard("📅", t(lang, "Aujourd'hui", "Today"), $today, t(lang, "pomodoro(s)", "pomodoro(s)"))
b.add statCard("🗓️", t(lang, "Cette semaine", "This week"), $week, t(lang, "pomodoro(s)", "pomodoro(s)"))
b.add statCard("🔥", t(lang, "Série", "Streak"), $streak, t(lang, "jour(s)", "day(s)"))
b.add statCard("🏆", t(lang, "Total", "Total"), $total, t(lang, "complétés", "completed"))
b.add "</div>"
b.add "<div class=\"card\"><h2>" & t(lang, "Aujourd'hui", "Today") & "</h2>" & if cfg.dailyGoal > 0:
"<p style=\"font-size:2.2rem;margin:.2rem 0;font-weight:700\">" & $today & " <span class=\"muted\">" & b.add "<div class='card'><h2>🎯 " & t(lang, "Objectif journalier", "Daily goal") & "</h2>"
t(lang, "pomodoro(s)", "pomodoro(s)") & "</span></p>" & b.add "<p class='big-number'>" & $today & " / " & $cfg.dailyGoal & "</p>"
(if cfg.dailyGoal > 0: b.add "<div class='progress'><div style='width:" &
"<p class=\"muted\">" & t(lang, "Objectif", "Goal") & " : " & $today & " / " & $cfg.dailyGoal & "</p>" & $(if today >= cfg.dailyGoal: 100 else: today * 100 div cfg.dailyGoal) & "%'></div></div>"
"<div class=\"bar\" style=\"width:100%\"><div style=\"width:" & if today >= cfg.dailyGoal:
$(if today >= cfg.dailyGoal: 100 else: today * 100 div cfg.dailyGoal) & "%\"></div></div>" b.add "<p class='goalok'>🎉 " & t(lang, "Objectif atteint, bravo !", "Goal reached, congratulations!") & "</p>"
else: "") & else:
"</div>" b.add "<p class='hint'>💪 " & t(lang,
"Encore un petit effort — chaque pomodoro compte !",
"Keep going — every pomodoro counts!") & "</p>"
b.add "</div>"
b.add "<div class=\"grid\"><div class=\"card\"><h2>" & t(lang, "Cette semaine", "This week") & "</h2>" & b.add "<h2>📈 " & t(lang, "7 derniers jours", "Last 7 days") & "</h2>"
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $week & "</p></div>" & b.add "<div class='chart'>"
"<div class=\"card\"><h2>" & t(lang, "Série (streak)", "Streak") & "</h2>" &
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $streak & " <span class=\"muted\">" &
t(lang, "jour(s)", "day(s)") & "</span></p></div></div>"
b.add "<div class=\"card\"><h2>" & t(lang, "Total", "Total") & "</h2>" &
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $total & " <span class=\"muted\">" &
t(lang, "pomodoros complétés", "completed pomodoros") & "</span></p></div>"
b.add "<h2>" & t(lang, "7 derniers jours", "Last 7 days") & "</h2><table>" &
"<tr><th>" & t(lang, "Date", "Date") & "</th><th>" & t(lang, "Pomodoros", "Pomodoros") & "</th></tr>"
for (date, n) in days: for (date, n) in days:
b.add "<tr><td>" & htmlEscape(date) & "</td><td>" & $n & " <span class=\"bar\" style=\"width:120px\"><div style=\"width:" & let dayLabel = date[5 .. 6] & "/" & date[8 .. 9]
$(n * 100 div maxv) & "%\"></div></span></td></tr>" let h = if n == 0: 6 else: 14 + n * 100 div maxv
b.add "</table>" b.add "<div class='barcol'>" &
"<div class='barval'>" & (if n > 0: $n else: "") & "</div>" &
"<div class='barfill' style='height:" & $h & "px'></div>" &
"<div class='barlabel'>" & htmlEscape(dayLabel) & "</div></div>"
b.add "</div>"
b.add "<p class='hint'>💾 " & t(lang,
"Un export CSV est disponible depuis le menu de l'icône.",
"A CSV export is available from the icon menu.") & "</p>"
b.add "<p class=\"muted\">" & t(lang, pageShell(lang, t(lang, "Statistiques", "Statistics"), b, "stats")
"Un export CSV est disponible depuis le menu de l'icône (Exporter les statistiques).",
"A CSV export is available from the icon menu (Export statistics).") & "</p>"
pageShell(lang, t(lang, "Statistiques", "Statistics"), b)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Page réglages # Page réglages
@@ -376,83 +480,101 @@ proc renderSettings*(cfg: Config): string =
var profileOpts = "" var profileOpts = ""
for p in cfg.profiles: for p in cfg.profiles:
let sel = if p.name == cfg.activeProfile: " selected" else: "" let sel = if p.name == cfg.activeProfile: " selected" else: ""
profileOpts.add "<option value=\"" & htmlEscape(p.name) & "\"@@" & sel & ">" & profileOpts.add "<option value='" & htmlEscape(p.name) & "'" & sel & ">" &
htmlEscape(p.name) & "</option>" htmlEscape(p.name) & "</option>"
let langOpts = let langOpts =
"<option value=\"auto\"@@" & (if cfg.lang == "auto": " selected" else: "") & ">" & "<option value='auto'" & (if cfg.lang == "auto": " selected" else: "") & ">🌐 " &
t(lang, "Automatique", "Automatic") & "</option>" & t(lang, "Automatique", "Automatic") & "</option>" &
"<option value=\"fr\"@@" & (if cfg.lang == "fr": " selected" else: "") & ">Français</option>" & "<option value='fr'" & (if cfg.lang == "fr": " selected" else: "") & ">🇫🇷 Français</option>" &
"<option value=\"en\"@@" & (if cfg.lang == "en": " selected" else: "") & ">English</option>" "<option value='en'" & (if cfg.lang == "en": " selected" else: "") & ">🇬🇧 English</option>"
var b = "" var b = ""
b.add "<h1>" & t(lang, "Réglages", "Settings") & "</h1>" b.add "<form method='post' action='/settings' onsubmit='syncReminders()'>"
b.add "<form method=\"post\" action=\"/settings\" onsubmit=\"syncReminders()\">"
b.add "<div class=\"grid\"><div>" # --- Minuteur ---
b.add "<label>" & t(lang, "Travail (min)", "Work (min)") & "</label>" & b.add "<div class='card'><h2>⏱️ " & t(lang, "Minuteur", "Timer") & "</h2>"
"<input type=\"number\" name=\"workMin\" min=\"1\" value=\"" & $cfg.workMin & "\">" b.add "<p class='hint'>" & t(lang,
b.add "<label>" & t(lang, "Pause courte (min)", "Short break (min)") & "</label>" & "Valeurs de base utilisées quand aucun profil ne correspond.",
"<input type=\"number\" name=\"shortBreakMin\" min=\"1\" value=\"" & $cfg.shortBreakMin & "\">" "Base values used when no profile matches.") & "</p>"
b.add "<label>" & t(lang, "Pause longue (min)", "Long break (min)") & "</label>" & b.add "<div class='grid2'>"
"<input type=\"number\" name=\"longBreakMin\" min=\"1\" value=\"" & $cfg.longBreakMin & "\">" b.add numField("💼", t(lang, "Travail (min)", "Work (min)"), "workMin", cfg.workMin, 1)
b.add "<label>" & t(lang, "Cycles avant pause longue", "Cycles before long break") & "</label>" & b.add numField("☕", t(lang, "Pause courte (min)", "Short break (min)"), "shortBreakMin", cfg.shortBreakMin, 1)
"<input type=\"number\" name=\"cyclesBeforeLongBreak\" min=\"1\" value=\"" & $cfg.cyclesBeforeLongBreak & "\">" b.add numField("🛋️", t(lang, "Pause longue (min)", "Long break (min)"), "longBreakMin", cfg.longBreakMin, 1)
b.add "<label>" & t(lang, "Objectif journalier (0 = aucun)", "Daily goal (0 = none)") & "</label>" & b.add numField("🔁", t(lang, "Cycles avant pause longue", "Cycles before long break"),
"<input type=\"number\" name=\"dailyGoal\" min=\"0\" value=\"" & $cfg.dailyGoal & "\">" "cyclesBeforeLongBreak", cfg.cyclesBeforeLongBreak, 1)
b.add "</div><div>" b.add "</div></div>"
b.add "<label>" & t(lang, "Langue", "Language") & "</label>" & # --- Objectif et langue ---
"<select name=\"lang\">" & langOpts & "</select>" b.add "<div class='card'><h2>🎯 " & t(lang, "Objectif et langue", "Goal and language") & "</h2>"
b.add "<label>" & t(lang, "Profil actif", "Active profile") & "</label>" & b.add "<div class='grid2'>"
"<select name=\"activeProfile\">" & profileOpts & "</select>" b.add numField("🎯", t(lang, "Objectif journalier (0 = aucun)", "Daily goal (0 = none)"),
"dailyGoal", cfg.dailyGoal, 0)
b.add "<label class='field'><span class='flabel'>🌐 " & t(lang, "Langue", "Language") & "</span>" &
"<select name='lang'>" & langOpts & "</select></label>"
b.add "</div></div>"
b.add "<p style=\"margin-top:.9rem\">" # --- Profil actif ---
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"soundEnabled\"@@" & b.add "<div class='card'><h2>👤 " & t(lang, "Profil actif", "Active profile") & "</h2>"
(if cfg.soundEnabled: " checked" else: "") & "> " & t(lang, "Son en fin de période", "Sound at end of period") & "</label>" b.add "<p class='hint'>" & t(lang,
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"doNotDisturb\"@@" & "Détermine les durées utilisées pour les prochaines périodes.",
(if cfg.doNotDisturb: " checked" else: "") & "> " & t(lang, "Ne pas déranger", "Do not disturb") & "</label>" "Determines the durations used for the next periods.") & "</p>"
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"autoStart\"@@" & b.add "<label class='field'><span class='flabel'>📦 " & t(lang, "Profil", "Profile") & "</span>" &
(if cfg.autoStart: " checked" else: "") & "> " & t(lang, "Démarrage avec Windows", "Start with Windows") & "</label>" "<select name='activeProfile'>" & profileOpts & "</select></label>"
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"floatingWindow\"@@" & b.add "</div>"
(if cfg.floatingWindow: " checked" else: "") & "> " & t(lang, "Fenêtre flottante", "Floating window") & "</label>"
b.add "</p></div></div>" # --- Notifications et système ---
b.add "<div class='card'><h2>🔔 " & t(lang, "Notifications et système", "Notifications and system") & "</h2>"
b.add checkRow("soundEnabled", cfg.soundEnabled, "🔊", t(lang, "Son en fin de période", "Sound at end of period"))
b.add checkRow("doNotDisturb", cfg.doNotDisturb, "🌙", t(lang, "Ne pas déranger", "Do not disturb"))
b.add checkRow("autoStart", cfg.autoStart, "🚀", t(lang, "Démarrage avec Windows", "Start with Windows"))
b.add checkRow("floatingWindow", cfg.floatingWindow, "🖥️", t(lang, "Fenêtre flottante", "Floating window"))
b.add "</div>"
# --- Rappels santé (éditeur client) --- # --- Rappels santé (éditeur client) ---
b.add "<h2>" & t(lang, "Rappels santé", "Health reminders") & "</h2>" b.add "<div class='card'><h2>💪 " & t(lang, "Rappels santé", "Health reminders") & "</h2>"
b.add "<div id=\"reminders\"></div>" b.add "<p class='hint'>" & t(lang,
b.add "<button type=\"button\" class=\"btn\" onclick=\"addReminder()\" style=\"margin-top:.6rem\">" & "Ajoutez, modifiez ou désactivez les rappels affichés pendant les pauses.",
"Add, edit or disable the reminders shown during breaks.") & "</p>"
b.add "<div id='reminders'></div>"
b.add "<button type='button' class='btn ghost' onclick='addReminder()' style='margin-top:.8rem'>➕ " &
t(lang, "Ajouter un rappel", "Add a reminder") & "</button>" t(lang, "Ajouter un rappel", "Add a reminder") & "</button>"
b.add "<input type=\"hidden\" name=\"reminders_json\" id=\"reminders_json\">" b.add "<input type='hidden' name='reminders_json' id='reminders_json'>"
b.add "</div>"
b.add "<p style=\"margin-top:1.4rem\"><button type=\"submit\" class=\"btn\">" & b.add "<p class='savezone'><button type='submit' class='btn big'>💾 " &
t(lang, "Enregistrer", "Save") & "</button></p>" t(lang, "Enregistrer les réglages", "Save settings") & "</button></p>"
b.add "</form>" b.add "</form>"
b.add "<script>" b.add "<script>"
b.add "var reminders = " & remindersJson(cfg.healthReminders) & ";" b.add "var reminders = " & remindersJson(cfg.healthReminders) & ";"
b.add """var R = function(fr, en){ return document.documentElement.lang === 'fr' ? fr : en; }; b.add """var R = function(fr, en){ return document.documentElement.lang === 'fr' ? fr : en; };
function esc(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function renderReminders(){ function renderReminders(){
var el = document.getElementById('reminders'); var el = document.getElementById('reminders');
if (reminders.length === 0){ el.innerHTML = '<p class="muted">' + R('Aucun rappel.','No reminders.') + '</p>'; return; } if (reminders.length === 0){
var html = '<table><tr><th></th><th>' + R('Texte','Text') + '</th><th></th><th></th></tr>'; el.innerHTML = '<div class="empty">' + R('🌱 Aucun rappel pour le moment.','🌱 No reminders yet.') + '</div>';
return;
}
var html = '<div class="remlist">';
for (var i = 0; i < reminders.length; i++){ for (var i = 0; i < reminders.length; i++){
var r = reminders[i]; var r = reminders[i];
var safe = r.text.replace(/&/g,'&amp;').replace(/</g,'&lt;'); html += '<div class="remrow' + (r.enabled ? '' : ' off') + '">' +
html += '<tr><td><input type="checkbox" ' + (r.enabled ? 'checked' : '') + ' onchange="toggleReminder(' + i + ')"></td>' + '<input type="checkbox" class="chk" ' + (r.enabled ? 'checked' : '') + ' onchange="toggleReminder(' + i + ')">' +
'<td>' + safe + '</td>' + '<span class="remtext">' + esc(r.text) + '</span>' +
'<td><button type="button" onclick="editReminder(' + i + ')">' + R('Modifier','Edit') + '</button></td>' + '<button type="button" class="mini" onclick="editReminder(' + i + ')">✏️ ' + R('Modifier','Edit') + '</button>' +
'<td><button type="button" onclick="removeReminder(' + i + ')">' + R('Supprimer','Remove') + '</button></td></tr>'; '<button type="button" class="mini danger" onclick="removeReminder(' + i + ')">🗑️</button>' +
'</div>';
} }
html += '</table>'; html += '</div>';
el.innerHTML = html; el.innerHTML = html;
} }
function addReminder(){ function addReminder(){
var t = prompt(R('Nouveau rappel santé :','New health reminder:')); var t = prompt(R('💪 Nouveau rappel santé :','💪 New health reminder:'));
if (t && t.trim().length > 0){ reminders.push({text: t.trim(), enabled: true}); renderReminders(); } if (t && t.trim().length > 0){ reminders.push({text: t.trim(), enabled: true}); renderReminders(); }
} }
function editReminder(i){ function editReminder(i){
var t = prompt(R('Modifier le rappel :','Edit reminder:'), reminders[i].text); var t = prompt(R('✏️ Modifier le rappel :','✏️ Edit reminder:'), reminders[i].text);
if (t && t.trim().length > 0){ reminders[i].text = t.trim(); renderReminders(); } if (t && t.trim().length > 0){ reminders[i].text = t.trim(); renderReminders(); }
} }
function removeReminder(i){ reminders.splice(i, 1); renderReminders(); } function removeReminder(i){ reminders.splice(i, 1); renderReminders(); }
@@ -462,17 +584,20 @@ renderReminders();
""" """
b.add "</script>" b.add "</script>"
pageShell(lang, t(lang, "Réglages", "Settings"), b) pageShell(lang, t(lang, "Réglages", "Settings"), b, "settings")
proc renderSaved(cfg: Config): string = proc renderSaved(cfg: Config): string =
let lang = resolveLang(cfg) let lang = resolveLang(cfg)
var b = "<h1>" & t(lang, "Réglages enregistrés", "Settings saved") & " ✓</h1>" & var b = "<div class='card saved'><div class='savedemoji'>🎉</div>" &
"<div class=\"card\"><p>" & "<h2>" & t(lang, "Réglages enregistrés !", "Settings saved!") & "</h2>" &
t(lang, "Votre configuration a été enregistrée et appliquée.", "<p>" & t(lang,
"Your configuration has been saved and applied.") & "Votre configuration a été enregistrée et appliquée immédiatement.",
"</p><p><a class=\"btn\" href=\"/settings\">" & t(lang, "Retour aux réglages", "Back to settings") & "</a> " & "Your configuration has been saved and applied immediately.") & "</p>" &
"<a class=\"btn\" href=\"/stats\">" & t(lang, "Voir les statistiques", "View statistics") & "</a></p></div>" "<p><a class='btn' href='/settings'>⚙️ " & t(lang, "Retour aux réglages", "Back to settings") & "</a> " &
pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b) "<a class='btn ghost' href='/stats'>📊 " & t(lang, "Voir les statistiques", "View statistics") & "</a></p></div>"
pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b, "settings")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Application d'un formulaire de réglages # Application d'un formulaire de réglages
+3 -3
View File
@@ -76,7 +76,7 @@ suite "rendering":
check "Pomodoro" in renderHelp(cfg) check "Pomodoro" in renderHelp(cfg)
check "Statistiques" in renderStats(cfg) check "Statistiques" in renderStats(cfg)
check "Réglages" in renderSettings(cfg) check "Réglages" in renderSettings(cfg)
check "name=\"workMin\"" in renderSettings(cfg) check "name='workMin'" in renderSettings(cfg)
check "reminders_json" in renderSettings(cfg) check "reminders_json" in renderSettings(cfg)
suite "integration (local HTTP server)": suite "integration (local HTTP server)":
@@ -105,7 +105,7 @@ suite "integration (local HTTP server)":
check "Statistiques" in stats check "Statistiques" in stats
let settings = client.getContent(base & "/settings") let settings = client.getContent(base & "/settings")
check "name=\"workMin\"" in settings check "name='workMin'" in settings
# Sauvegarde via POST (comme le ferait le navigateur). # Sauvegarde via POST (comme le ferait le navigateur).
let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" & let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" &
@@ -129,6 +129,6 @@ suite "integration (local HTTP server)":
# La page réglages reflète les nouvelles valeurs. # La page réglages reflète les nouvelles valeurs.
let settings2 = client.getContent(base & "/settings") let settings2 = client.getContent(base & "/settings")
check "value=\"33\"" in settings2 check "value='33'" in settings2
removeDir(dir) removeDir(dir)