From 162d574ff484712e08373fb72e7380f1e0850946 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 14 Aug 2026 22:26:57 -0400 Subject: [PATCH] =?UTF-8?q?design(web):=20refonte=20des=20pages=20aide=20/?= =?UTF-8?q?=20statistiques=20/=20r=C3=A9glages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/webserver.nim | 557 ++++++++++++++++++++++++--------------- tests/test_webserver.nim | 6 +- 2 files changed, 344 insertions(+), 219 deletions(-) diff --git a/src/webserver.nim b/src/webserver.nim index 69aa2cb..33a7135 100644 --- a/src/webserver.nim +++ b/src/webserver.nim @@ -143,11 +143,41 @@ proc t(lang: Lang, fr, en: string): string = # Gabarit de page # --------------------------------------------------------------------------- -proc pageShell(lang: Lang, title, body: string): string = - let nav = "" +proc statCard(emoji, label, value, unit: string): string = + ## Carte de statistique (emoji + libellé + grande valeur). + "
" & emoji & "
" & + "
" & label & "
" & + "
" & value & + (if unit.len > 0: " " & unit & "" else: "") & "
" + +proc numField(emoji, label, name: string, value, minVal: int): string = + ## Champ numérique libellé du formulaire de réglages. + "" + +proc checkRow(name: string, checked: bool, emoji, label: string): string = + ## Ligne de case à cocher du formulaire de réglages. + "" + +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 = "" + let header = "
" & + "" & + "

NimPulse

" & + t(lang, + "Le minuteur Pomodoro qui prend soin de vous", + "The Pomodoro timer that takes care of you") & + "

" + let footer = "" result = """ @@ -156,37 +186,111 @@ proc pageShell(lang: Lang, title, body: string): string = NimPulse — """ & title & """ -""" & nav & body & "" +
+""" & header & nav & body & footer & """ +
+ +""" + # --------------------------------------------------------------------------- # Page d'aide @@ -195,116 +299,114 @@ input[type=checkbox]{width:auto} proc renderHelp*(cfg: Config): string = let lang = resolveLang(cfg) var b = "" - b.add "

" & t(lang, - "Aide — Comment fonctionne NimPulse", - "Help — How NimPulse works") & "

" - b.add "

" & t(lang, "Le principe", "The concept") & "

" & - t(lang, - "NimPulse est un minuteur Pomodoro qui vit dans la zone de notification (tray). " & - "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…).") & "

" + b.add "

🍅 " & t(lang, + "NimPulse alterne des périodes de travail concentré et des pauses, et profite des pauses pour vous rappeler de prendre soin de vous.", + "NimPulse alternates focused work sessions with breaks, and uses breaks to remind you to take care of yourself.") & "

" - b.add "

" & t(lang, "Premiers pas", "Getting started") & "

" + b.add "

🧭 " & t(lang, "Comment ça marche", "How it works") & "

" + b.add "
" + b.add "
1
💼 " & t(lang, "Travaillez", "Work") & "

" & t(lang, + "Lancez une période de travail concentré — 25 minutes par défaut.", + "Start a focused work session — 25 minutes by default.") & "

" + b.add "
2
☕ " & t(lang, "Pausez", "Break") & "

" & t(lang, + "Reposez-vous pendant une pause courte, ou longue après plusieurs cycles.", + "Rest during a short break, or a long one after several cycles.") & "

" + b.add "
3
💪 " & t(lang, "Bougez", "Move") & "

" & t(lang, + "Pendant la pause, NimPulse vous rappelle de marcher, boire ou vous étirer.", + "During the break, NimPulse reminds you to walk, drink or stretch.") & "

" + b.add "
" - b.add "

" & t(lang, "Les périodes", "The periods") & "

" & - t(lang, - "Un cycle classique : travail (25 min), pause courte (5 min), et après plusieurs cycles, " & - "une pause longue (15 min). Le nombre de cycles avant la pause longue est configurable, " & - "ainsi que les durées via les profils :", - "A classic cycle: work (25 min), short break (5 min), and after several cycles a long " & - "break (15 min). The number of cycles before the long break is configurable, as are the " & - "durations via the profiles:") & "

" & - "" & - "" & - "
" & - t(lang, "Profil", "Profile") & "" & t(lang, "Travail", "Work") & "" & - t(lang, "Pause courte", "Short break") & "" & t(lang, "Pause longue", "Long break") & "" & - t(lang, "Cycles", "Cycles") & "
" & t(lang, "Classique", "Classic") & "25 min5 min15 min4
Deep Work50 min10 min20 min4
Sprint15 min3 min10 min6
" & - "

" & t(lang, - "Le profil actif se choisit dans le menu de l'icône (« Profils »).", - "The active profile is chosen from the icon menu (“Profiles”).") & "

" + b.add "

🚀 " & t(lang, "Premiers pas", "Getting started") & "

" + b.add "
" - b.add "

" & t(lang, "Pendant une pause", "During a break") & "

" + b.add "

⏱️ " & t(lang, "Les périodes", "The periods") & "

" + b.add "

" & 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:") & "

" + b.add "" + b.add "" + b.add "" + b.add "
👤 " & t(lang, "Profil", "Profile") & "💼 " & t(lang, "Travail", "Work") & + "☕ " & t(lang, "Pause courte", "Short break") & "🛋️ " & t(lang, "Pause longue", "Long break") & + "🔁 " & t(lang, "Cycles", "Cycles") & "
" & t(lang, "Classique", "Classic") & "25 min5 min15 min4
Deep Work50 min10 min20 min4
Sprint15 min3 min10 min6
" + b.add "

" & t(lang, + "Le profil actif se choisit dans le menu de l'icône (« Profils »).", + "The active profile is chosen from the icon menu (“Profiles”).") & "

" - b.add "

" & t(lang, "Rappels santé", "Health reminders") & "

" & - t(lang, - "Les rappels sont 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.", - "Reminders are fully editable from the Settings page: add, edit, remove or disable your " & - "own activities. Only enabled reminders are shown.") & "

" + b.add "

☕ " & t(lang, "Pendant une pause", "During a break") & "

" + b.add "
" - b.add "

" & t(lang, "Statistiques et objectif", "Statistics and goal") & "

" & - t(lang, - "La page Statistiques affiche vos pomodoros du jour, de la semaine, votre série de jours " & - "consécutifs et le total. Vous pouvez définir 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.", - "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.") & "

" + b.add "

💪 " & t(lang, "Rappels santé", "Health reminders") & "

" + b.add "

" & t(lang, + "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.", + "Fully editable from the ⚙️ Settings page: add, edit, remove or disable your own activities. Only enabled reminders are shown.") & "

" - b.add "

" & t(lang, "Réglages", "Settings") & "

" + b.add "

📊 " & t(lang, "Statistiques et objectif", "Statistics and goal") & "

" + b.add "

" & t(lang, + "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.", + "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.") & "

" - b.add "

" & t(lang, "Astuces", "Tips") & "

" + b.add "

⚙️ " & t(lang, "Réglages", "Settings") & "

" + b.add "
" - b.add "

" & t(lang, "Raccourcis", "Shortcuts") & "

" & - "" & - "" & - "" & - "" & - "
" & t(lang, "Action", "Action") & "" & t(lang, "Raccourci", "Shortcut") & "
" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "Ctrl+Alt+P
" & t(lang, "Menu contextuel", "Context menu") & "" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "
" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "" & t(lang, "Double-clic", "Double-click") & "
" + b.add "

💡 " & t(lang, "Astuces", "Tips") & "

" + b.add "
" + + b.add "

⌨️ " & t(lang, "Raccourcis", "Shortcuts") & "

" + b.add "
" + b.add "" + b.add "" + b.add "" + b.add "" + b.add "
" & t(lang, "Action", "Action") & "" & t(lang, "Raccourci", "Shortcut") & "
▶️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "Ctrl+Alt+P
📋 " & t(lang, "Menu contextuel", "Context menu") & "" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "
🖱️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "" & t(lang, "Double-clic", "Double-click") & "
" + + pageShell(lang, t(lang, "Aide", "Help"), b, "help") - pageShell(lang, t(lang, "Aide", "Help"), b) # --------------------------------------------------------------------------- # Page statistiques @@ -324,40 +426,42 @@ proc renderStats*(cfg: Config): string = if n > maxv: maxv = n var b = "" - b.add "

" & t(lang, "Statistiques", "Statistics") & "

" + b.add "
" + 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 "
" - b.add "

" & t(lang, "Aujourd'hui", "Today") & "

" & - "

" & $today & " " & - t(lang, "pomodoro(s)", "pomodoro(s)") & "

" & - (if cfg.dailyGoal > 0: - "

" & t(lang, "Objectif", "Goal") & " : " & $today & " / " & $cfg.dailyGoal & "

" & - "
= cfg.dailyGoal: 100 else: today * 100 div cfg.dailyGoal) & "%\">
" - else: "") & - "
" + if cfg.dailyGoal > 0: + b.add "

🎯 " & t(lang, "Objectif journalier", "Daily goal") & "

" + b.add "

" & $today & " / " & $cfg.dailyGoal & "

" + b.add "
" + if today >= cfg.dailyGoal: + b.add "

🎉 " & t(lang, "Objectif atteint, bravo !", "Goal reached, congratulations!") & "

" + else: + b.add "

💪 " & t(lang, + "Encore un petit effort — chaque pomodoro compte !", + "Keep going — every pomodoro counts!") & "

" + b.add "
" - b.add "

" & t(lang, "Cette semaine", "This week") & "

" & - "

" & $week & "

" & - "

" & t(lang, "Série (streak)", "Streak") & "

" & - "

" & $streak & " " & - t(lang, "jour(s)", "day(s)") & "

" - - b.add "

" & t(lang, "Total", "Total") & "

" & - "

" & $total & " " & - t(lang, "pomodoros complétés", "completed pomodoros") & "

" - - b.add "

" & t(lang, "7 derniers jours", "Last 7 days") & "

" & - "" + b.add "

📈 " & t(lang, "7 derniers jours", "Last 7 days") & "

" + b.add "
" for (date, n) in days: - b.add "
" - b.add "
" & t(lang, "Date", "Date") & "" & t(lang, "Pomodoros", "Pomodoros") & "
" & htmlEscape(date) & "" & $n & "
" + let dayLabel = date[5 .. 6] & "/" & date[8 .. 9] + let h = if n == 0: 6 else: 14 + n * 100 div maxv + b.add "
" & + "
" & (if n > 0: $n else: "") & "
" & + "
" & + "
" & htmlEscape(dayLabel) & "
" + b.add "" + b.add "

💾 " & t(lang, + "Un export CSV est disponible depuis le menu de l'icône.", + "A CSV export is available from the icon menu.") & "

" - b.add "

" & t(lang, - "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).") & "

" + pageShell(lang, t(lang, "Statistiques", "Statistics"), b, "stats") - pageShell(lang, t(lang, "Statistiques", "Statistics"), b) # --------------------------------------------------------------------------- # Page réglages @@ -376,83 +480,101 @@ proc renderSettings*(cfg: Config): string = var profileOpts = "" for p in cfg.profiles: let sel = if p.name == cfg.activeProfile: " selected" else: "" - profileOpts.add "" let langOpts = - "" & - "" & - "" + "" & + "" var b = "" - b.add "

" & t(lang, "Réglages", "Settings") & "

" - b.add "
" + b.add "" - b.add "
" - b.add "" & - "" - b.add "" & - "" - b.add "" & - "" - b.add "" & - "" - b.add "" & - "" - b.add "
" + # --- Minuteur --- + b.add "

⏱️ " & t(lang, "Minuteur", "Timer") & "

" + b.add "

" & t(lang, + "Valeurs de base utilisées quand aucun profil ne correspond.", + "Base values used when no profile matches.") & "

" + b.add "
" + b.add numField("💼", t(lang, "Travail (min)", "Work (min)"), "workMin", cfg.workMin, 1) + b.add numField("☕", t(lang, "Pause courte (min)", "Short break (min)"), "shortBreakMin", cfg.shortBreakMin, 1) + b.add numField("🛋️", t(lang, "Pause longue (min)", "Long break (min)"), "longBreakMin", cfg.longBreakMin, 1) + b.add numField("🔁", t(lang, "Cycles avant pause longue", "Cycles before long break"), + "cyclesBeforeLongBreak", cfg.cyclesBeforeLongBreak, 1) + b.add "
" - b.add "" & - "" - b.add "" & - "" + # --- Objectif et langue --- + b.add "

🎯 " & t(lang, "Objectif et langue", "Goal and language") & "

" + b.add "
" + b.add numField("🎯", t(lang, "Objectif journalier (0 = aucun)", "Daily goal (0 = none)"), + "dailyGoal", cfg.dailyGoal, 0) + b.add "" + b.add "
" - b.add "

" - b.add "" - b.add "" - b.add "" - b.add "" - b.add "

" + # --- Profil actif --- + b.add "

👤 " & t(lang, "Profil actif", "Active profile") & "

" + b.add "

" & t(lang, + "Détermine les durées utilisées pour les prochaines périodes.", + "Determines the durations used for the next periods.") & "

" + b.add "" + b.add "
" + + # --- Notifications et système --- + b.add "

🔔 " & t(lang, "Notifications et système", "Notifications and system") & "

" + 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 "
" # --- Rappels santé (éditeur client) --- - b.add "

" & t(lang, "Rappels santé", "Health reminders") & "

" - b.add "
" - b.add "" - b.add "" + b.add "" + b.add "" - b.add "

" + b.add "

" b.add "
" b.add "" - pageShell(lang, t(lang, "Réglages", "Settings"), b) + pageShell(lang, t(lang, "Réglages", "Settings"), b, "settings") + proc renderSaved(cfg: Config): string = let lang = resolveLang(cfg) - var b = "

" & t(lang, "Réglages enregistrés", "Settings saved") & " ✓

" & - "

" & - t(lang, "Votre configuration a été enregistrée et appliquée.", - "Your configuration has been saved and applied.") & - "

" & t(lang, "Retour aux réglages", "Back to settings") & " " & - "" & t(lang, "Voir les statistiques", "View statistics") & "

" - pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b) + var b = "
🎉
" & + "

" & t(lang, "Réglages enregistrés !", "Settings saved!") & "

" & + "

" & t(lang, + "Votre configuration a été enregistrée et appliquée immédiatement.", + "Your configuration has been saved and applied immediately.") & "

" & + "

⚙️ " & t(lang, "Retour aux réglages", "Back to settings") & " " & + "📊 " & t(lang, "Voir les statistiques", "View statistics") & "

" + pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b, "settings") + # --------------------------------------------------------------------------- # Application d'un formulaire de réglages diff --git a/tests/test_webserver.nim b/tests/test_webserver.nim index 0f65451..bda2b28 100644 --- a/tests/test_webserver.nim +++ b/tests/test_webserver.nim @@ -76,7 +76,7 @@ suite "rendering": check "Pomodoro" in renderHelp(cfg) check "Statistiques" in renderStats(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) suite "integration (local HTTP server)": @@ -105,7 +105,7 @@ suite "integration (local HTTP server)": check "Statistiques" in stats let settings = client.getContent(base & "/settings") - check "name=\"workMin\"" in settings + check "name='workMin'" in settings # Sauvegarde via POST (comme le ferait le navigateur). 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. let settings2 = client.getContent(base & "/settings") - check "value=\"33\"" in settings2 + check "value='33'" in settings2 removeDir(dir)