diff --git a/README.md b/README.md index aa8a8a8..bc82013 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,8 @@ Recommended deployment: The `Training` menu provides a non-race progression view for registered drivers: +The default view is intentionally simple: four key statistics, the next lap target, today's 20-minute plan and active rewards. Goal administration, mobile sharing, detailed history and the club comparison remain available in collapsed sections when needed. + - durable lifetime valid-lap ledger synchronized from stored session results - practice days, recorded sessions, personal best, overall/recent averages, controlled-lap streak, and recent consistency spread - daily history for the latest practice days and recent personal-best improvement @@ -192,7 +194,7 @@ Judging corrections update ledger laps while the source session still exists. On Each registered driver can have an optional personal mobile link: 1. Open `Training` and select the driver. -2. Click `Enable mobile profile`. +2. Open `Goals and mobile profile`, then click `Enable mobile profile`. 3. Copy or open the generated link. 4. Use `Create new link` to revoke the previous URL, or `Disable profile` to turn sharing off. diff --git a/README.sv.md b/README.sv.md index 1df2b02..a68a481 100644 --- a/README.sv.md +++ b/README.sv.md @@ -161,6 +161,8 @@ Rekommenderad deploy: Menyn `Träning` ger förarna utveckling och delmål även mellan tävlingarna: +Grundvyn är avsiktligt enkel: fyra nyckeltal, nästa varvmål, dagens 20-minuterspass och aktiva belöningar. Administration av mål, mobildelning, detaljhistorik och klubbjämförelse finns kvar i utfällbara sektioner när de behövs. + - beständig logg med totalt antal giltiga varv synkad från sparade sessionsresultat - träningsdagar, antal sessioner, personbästa, total-/30-varvssnitt, kontrollerad varvserie och spridning - daglig historik för de senaste träningsdagarna och förbättring mot tidigare personbästa @@ -181,7 +183,7 @@ Domarkorrigeringar uppdaterar loggade varv så länge källsessionen finns kvar. Varje registrerad förare kan få en valfri personlig mobillänk: 1. Öppna `Träning` och välj föraren. -2. Klicka `Aktivera mobilprofil`. +2. Öppna `Mål och mobilprofil` och klicka sedan `Aktivera mobilprofil`. 3. Kopiera eller öppna länken som skapas. 4. Använd `Skapa ny länk` för att återkalla den gamla eller `Stäng av profil` för att stoppa delningen. diff --git a/docs/javascript-architecture.md b/docs/javascript-architecture.md index 8881c14..023111b 100644 --- a/docs/javascript-architecture.md +++ b/docs/javascript-architecture.md @@ -431,8 +431,8 @@ Type: Owns: - driver selector -- training stat cards and recommendation display -- automatic next-level card and daily practice history +- simple default view with four key statistics, recommendation, next level, and active rewards +- progressive disclosure for goal/share administration, detailed history, and club comparison - milestone/reward create, claim, and delete actions - mobile-responsive training markup diff --git a/src/app.js b/src/app.js index 2f89d43..49796f8 100644 --- a/src/app.js +++ b/src/app.js @@ -304,6 +304,8 @@ const TRANSLATIONS = { "training.block_push": "Maxfokus: hitta gränsen", "training.block_control": "80-90%: höj lägstanivån", "training.new_goal": "Nytt delmål", + "training.manage_goals_sharing": "Mål och mobilprofil", + "training.manage_goals_sharing_hint": "Skapa belöningsmål eller hantera förarens delningslänk.", "training.target_laps": "Antal varv", "training.quick_targets": "Snabbval för antal varv", "training.goal_name": "Namn på delmålet", @@ -330,6 +332,8 @@ const TRANSLATIONS = { "training.last_activity": "Senaste aktivitet", "training.no_activity": "Ingen aktivitet ännu", "training.history": "Träningshistorik per dag", + "training.detailed_stats": "Mer statistik", + "training.detailed_stats_hint": "Snitt, varvserie och historik per träningsdag.", "training.no_history": "Ingen träningshistorik finns ännu.", "training.date": "Datum", "training.laps": "Varv", @@ -347,6 +351,7 @@ const TRANSLATIONS = { "training.enable_profile": "Aktivera mobilprofil", "training.replace_link_confirm": "Den gamla länken slutar fungera direkt. Vill du skapa en ny?", "training.club_progress": "Klubbens träningsöversikt", + "training.club_progress_hint": "Jämför totalvarv och nästa mål när det behövs.", "training.next_target": "Nästa mål", "mode.track": "Sponsor Event", "mode.race": "Race", @@ -1045,11 +1050,11 @@ const TRANSLATIONS = { "guide.training_title": "Träning, utveckling och belöningar", "guide.training_1": "1. Registrera föraren och koppla den personliga transpondern under Förare. Endast passeringar med ett registrerat förar-ID kan räknas i träningsprofilen.", "guide.training_2": "2. Kör Free Practice, Open Practice eller en vanlig racesession. Giltiga varv kopieras automatiskt till en beständig träningslogg; korta/långa ogiltiga varv räknas inte.", - "guide.training_3": "3. Öppna Träning och välj förare. Sidan visar totalvarv, träningsdagar, personbästa, snitt, senaste 30 varv, kontrollerad serie och historik per dag.", + "guide.training_3": "3. Öppna Träning och välj förare. Grundvyn visar bara totalvarv, träningsdagar, personbästa, jämnhet, nästa mål och dagens pass. Öppna Mer statistik eller Klubbens träningsöversikt bara när det behövs.", "guide.training_4": "4. Nästa automatiska nivå räknas utan konfiguration. Skapa egna delmål om klubben eller en förälder vill koppla en fysisk belöning till exempelvis 500, 1000 eller 1500 varv.", "guide.training_5": "5. När målet nåtts kan belöningen markeras som utdelad. Direktlänken `/?view=training&driver=` kan bokmärkas på förarens mobil.", "guide.training_6": "6. Träningsloggen uppdateras av domarkorrigeringar medan sessionen finns, men behålls när sessionen senare raderas. Full dataexport i Inställningar inkluderar både logg och mål.", - "guide.training_7": "7. Under Mobilprofil för förare klickar du Aktivera mobilprofil. Kopiera den personliga länken och öppna den på förarens eller förälderns mobil.", + "guide.training_7": "7. Öppna Mål och mobilprofil och klicka Aktivera mobilprofil. Kopiera den personliga länken och öppna den på förarens eller förälderns mobil.", "guide.training_8": "8. Mobilprofilen är skrivskyddad och visar bara vald förares statistik. Där finns också en lokal 20-minuterstimer som växlar från 10 minuter maxfokus till 10 minuter kontrollerad körning.", "guide.training_9": "9. Klicka Skapa ny länk eller Stäng av profil om länken spridits fel. Publicera bara `/public-training/*`, `/src/public_training.js` och `/src/public_training.css` externt, aldrig hela admin-sidan eller `/api/state`.", "overlay.title": "Overlay", @@ -1193,6 +1198,8 @@ const TRANSLATIONS = { "training.block_push": "Maximum focus: find the limit", "training.block_control": "80-90%: raise the floor", "training.new_goal": "New milestone", + "training.manage_goals_sharing": "Goals and mobile profile", + "training.manage_goals_sharing_hint": "Create reward goals or manage the driver's share link.", "training.target_laps": "Target laps", "training.quick_targets": "Quick lap targets", "training.goal_name": "Milestone name", @@ -1219,6 +1226,8 @@ const TRANSLATIONS = { "training.last_activity": "Latest activity", "training.no_activity": "No activity yet", "training.history": "Daily practice history", + "training.detailed_stats": "More statistics", + "training.detailed_stats_hint": "Averages, lap streak and daily practice history.", "training.no_history": "No practice history is available yet.", "training.date": "Date", "training.laps": "Laps", @@ -1236,6 +1245,7 @@ const TRANSLATIONS = { "training.enable_profile": "Enable mobile profile", "training.replace_link_confirm": "The old link will stop working immediately. Create a new one?", "training.club_progress": "Club practice overview", + "training.club_progress_hint": "Compare total laps and next targets when needed.", "training.next_target": "Next target", "mode.track": "Track Event", "mode.race": "Race", @@ -1934,11 +1944,11 @@ const TRANSLATIONS = { "guide.training_title": "Practice, development and rewards", "guide.training_1": "1. Register the driver and personal transponder under Drivers. Only passings linked to a registered driver ID count toward the practice profile.", "guide.training_2": "2. Run Free Practice, Open Practice or a regular race session. Valid laps are copied automatically to a durable training ledger; invalid short or long laps do not count.", - "guide.training_3": "3. Open Training and select a driver. The page shows total laps, practice days, personal best, averages, latest 30 laps, controlled streak and daily history.", + "guide.training_3": "3. Open Training and select a driver. The default view only shows total laps, practice days, personal best, consistency, the next target and today's session. Open More statistics or Club practice overview only when needed.", "guide.training_4": "4. The next automatic level needs no configuration. Add custom milestones when the club or a parent wants to attach a physical reward to targets such as 500, 1000 or 1500 laps.", "guide.training_5": "5. When a target is reached, mark its reward as delivered. Bookmark `/?view=training&driver=` on the driver's phone for direct access.", "guide.training_6": "6. The training ledger follows judging corrections while the source session exists, but remains when that session is later deleted. Full data export in Settings includes both ledger and goals.", - "guide.training_7": "7. Under Driver mobile profile, click Enable mobile profile. Copy the personal link and open it on the driver's or parent's phone.", + "guide.training_7": "7. Open Goals and mobile profile, then click Enable mobile profile. Copy the personal link and open it on the driver's or parent's phone.", "guide.training_8": "8. The mobile profile is read-only and only shows the selected driver's statistics. It also includes a local 20-minute timer that changes from 10 minutes maximum focus to 10 minutes controlled driving.", "guide.training_9": "9. Click Create new link or Disable profile if the link is shared incorrectly. Expose only `/public-training/*`, `/src/public_training.js` and `/src/public_training.css` externally, never the full admin site or `/api/state`.", "overlay.title": "Overlay", diff --git a/src/public_training.css b/src/public_training.css index 05e9861..87099a5 100644 --- a/src/public_training.css +++ b/src/public_training.css @@ -361,6 +361,27 @@ button { overflow-x: auto; } +.profile-disclosure > summary { + cursor: pointer; + list-style: none; +} + +.profile-disclosure > summary::-webkit-details-marker { + display: none; +} + +.profile-disclosure > summary > span { + color: var(--muted); + font-family: Orbitron, sans-serif; + font-size: 1.1rem; + transition: transform 160ms ease, color 160ms ease; +} + +.profile-disclosure[open] > summary > span { + color: var(--accent); + transform: rotate(45deg); +} + .profile-empty { margin: 0; color: var(--muted); diff --git a/src/public_training.js b/src/public_training.js index fe4f338..8befe25 100644 --- a/src/public_training.js +++ b/src/public_training.js @@ -224,20 +224,22 @@ function renderProfile(data) { -
-

${escapeHtml(t("goals"))}

-
${goalMarkup}
-
+ ${goals.length ? ` +
+

${escapeHtml(t("goals"))}

+
${goalMarkup}
+
+ ` : ""} -
-

${escapeHtml(t("history"))}

+
+

${escapeHtml(t("history"))}

+
${historyRows}
${escapeHtml(t("date"))}${escapeHtml(t("dayLaps"))}${escapeHtml(t("best"))}${escapeHtml(t("average"))}
-
+ `; bindTimerControls(); updateTimerDisplay(); diff --git a/src/styles.css b/src/styles.css index f9ddc0a..2054fe5 100644 --- a/src/styles.css +++ b/src/styles.css @@ -346,13 +346,112 @@ body { margin-top: 14px; } -.training-insight-grid { +.training-insight-grid, +.training-focus-grid { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(0, 1.2fr); gap: 14px; margin-top: 14px; } +.training-secondary { + display: grid; + gap: 10px; + margin-top: 16px; +} + +.training-disclosure { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 14px; + background: color-mix(in srgb, var(--panel) 94%, transparent); + box-shadow: var(--shadow); +} + +.training-disclosure > summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 14px 16px; + cursor: pointer; + list-style: none; +} + +.training-disclosure > summary::-webkit-details-marker { + display: none; +} + +.training-disclosure > summary > span:first-child { + display: grid; + gap: 3px; +} + +.training-disclosure > summary small { + color: var(--muted); + font-size: 0.78rem; + font-weight: 400; +} + +.training-disclosure-arrow { + color: var(--muted); + font-family: Orbitron, sans-serif; + font-size: 1.15rem; + transition: transform 160ms ease, color 160ms ease; +} + +.training-disclosure[open] .training-disclosure-arrow { + color: var(--accent); + transform: rotate(45deg); +} + +.training-disclosure-body { + padding: 16px; + border-top: 1px solid var(--line); +} + +.training-simple-section { + padding: 16px; + border: 1px solid var(--line); + border-radius: 13px; + background: color-mix(in srgb, var(--panel-2) 72%, transparent); +} + +.training-simple-section h3 { + margin: 0 0 14px; +} + +.training-simple-head, +.training-section-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.training-simple-head h3, +.training-section-title h3 { + margin: 0; +} + +.training-share-compact { + display: grid; + gap: 12px; +} + +.training-share-compact p { + margin: 0; + color: var(--muted); +} + +.training-share-compact .training-share-url { + grid-column: auto; +} + +.training-section-title { + margin: 18px 0 8px; +} + .training-milestone-card, .training-insights { border: 1px solid var(--line); @@ -2429,7 +2528,8 @@ select:focus { grid-template-columns: 1fr; } - .training-insight-grid { + .training-insight-grid, + .training-focus-grid { grid-template-columns: 1fr; } } diff --git a/src/training_views.js b/src/training_views.js index 38efc7a..d1fdcce 100644 --- a/src/training_views.js +++ b/src/training_views.js @@ -132,7 +132,7 @@ export function renderTrainingView(deps) { ${trainingStat(t("training.consistency"), stats.recentLapCount ? `${stats.consistencySpreadPct.toFixed(1)}%` : "-", t("training.last_30_laps"))} -
+
${escapeHtml(t("training.next_level"))} @@ -142,15 +142,6 @@ export function renderTrainingView(deps) { ${nextMilestone.currentLaps} / ${nextMilestone.targetLaps}
-
- ${trainingInsight(t("training.average_lap"), formatLap(stats.averageLapMs))} - ${trainingInsight(t("training.recent_average"), formatLap(stats.recentAverageMs))} - ${trainingInsight(t("training.controlled_streak"), t("training.lap_count", { laps: stats.controlledLapStreak }))} - ${trainingInsight(t("training.last_activity"), lastActivity)} -
-
- -

${escapeHtml(t("training.recommendation"))}

20 min
@@ -163,79 +154,100 @@ export function renderTrainingView(deps) {
- -
-

${escapeHtml(t("training.new_goal"))}

-
-
- -
- ${[500, 1000, 1500].map((value) => ``).join("")} -
- - - -
-
-
-
-
-

${escapeHtml(t("training.public_profile"))}

- ${escapeHtml(t(profileShare ? "training.public_profile_active" : "training.public_profile_off"))} -
-
-
-

${escapeHtml(t("training.public_profile_help"))}

-

${escapeHtml(t("training.public_profile_private"))}

+ ${goals.length ? ` +
+

${escapeHtml(t("training.goals"))}

${goals.length}
+
${goalMarkup}
+
+ ` : ""} + +
+
+ + ${escapeHtml(t("training.manage_goals_sharing"))}${escapeHtml(t("training.manage_goals_sharing_hint"))} + + + +
+
+
+

${escapeHtml(t("training.new_goal"))}

+
+ +
+ ${[500, 1000, 1500].map((value) => ``).join("")} +
+ + + +
+
+
+
+

${escapeHtml(t("training.public_profile"))}

+ ${escapeHtml(t(profileShare ? "training.public_profile_active" : "training.public_profile_off"))} +
+
+

${escapeHtml(t("training.public_profile_help"))}

+ ${profileShare ? ` + +
+ + + + +
+ ` : ` + + `} +
+
+
- ${profileShare ? ` - -
- - - - +
+ +
+ + ${escapeHtml(t("training.detailed_stats"))}${escapeHtml(t("training.detailed_stats_hint"))} + + + +
+
+ ${trainingInsight(t("training.average_lap"), formatLap(stats.averageLapMs))} + ${trainingInsight(t("training.recent_average"), formatLap(stats.recentAverageMs))} + ${trainingInsight(t("training.controlled_streak"), t("training.lap_count", { laps: stats.controlledLapStreak }))} + ${trainingInsight(t("training.last_activity"), lastActivity)} +
+
+

${escapeHtml(t("training.history"))}

+ ${stats.personalBestGainMs ? `${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}` : ""}
- ` : ` -
- +
+ + + ${dailyRows} +
${escapeHtml(t("training.date"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.average_lap"))}
- `} -
-
+ + -
-
-

${escapeHtml(t("training.history"))}

- ${stats.personalBestGainMs ? `${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}` : ""} -
-
- - - ${dailyRows} -
${escapeHtml(t("training.date"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.average_lap"))}
-
-
- -
-

${escapeHtml(t("training.club_progress"))}

${drivers.length}
-
- - - ${clubRows} -
#${escapeHtml(t("training.driver"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.training_days"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.next_target"))}
-
-
- -
-

${escapeHtml(t("training.goals"))}

${goals.length}
-
${goalMarkup}
-
+
+ + ${escapeHtml(t("training.club_progress"))}${escapeHtml(t("training.club_progress_hint"))} + + + +
+ + + ${clubRows} +
#${escapeHtml(t("training.driver"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.training_days"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.next_target"))}
+
+
+ `; document.getElementById("training-driver")?.addEventListener("change", (event) => {