diff --git a/app/src/components/Bibliotek.jsx b/app/src/components/Bibliotek.jsx index e05af5d..d8818a1 100644 --- a/app/src/components/Bibliotek.jsx +++ b/app/src/components/Bibliotek.jsx @@ -1,11 +1,10 @@ import { useState, useEffect } from "react"; import { - Button, Tag, InlineNotification, InlineLoading, - Tabs, Tab, TabList, TabPanels, TabPanel, + Button, InlineNotification, InlineLoading, TextInput, Modal, } from "@carbon/react"; -import { Add, TrashCan, Edit as EditIcon, ChevronRight } from "@carbon/icons-react"; -import PageShell, { PageTitle } from "./PageShell"; +import { Add, TrashCan, Edit as EditIcon, ChevronRight, Search } from "@carbon/icons-react"; +import PageShell, { SectionLabel, PageHeading, AccentChip } from "./PageShell"; import { fetchLibraryExercises, saveLibraryExercise, updateLibraryExercise, deleteLibraryExercise, fetchTemplates, saveTemplate, deleteTemplate, fetchTemplateNamesUsingExercise, @@ -17,6 +16,7 @@ import ExerciseForm from "./ExerciseForm"; export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { const [tabIndex, setTabIndex] = useState(initialTab); + const [exSearch, setExSearch] = useState(""); const [exercises, setExercises] = useState([]); const [exLoading, setExLoading] = useState(true); @@ -25,7 +25,7 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { const [editingEx, setEditingEx] = useState(null); const [savingEx, setSavingEx] = useState(false); - const [confirmDelete, setConfirmDelete] = useState(null); // { type: "exercise"|"template", id, name } + const [confirmDelete, setConfirmDelete] = useState(null); const [templates, setTemplates] = useState([]); const [tplLoading, setTplLoading] = useState(true); @@ -45,6 +45,10 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { .finally(() => setTplLoading(false)); }, []); + const filteredExercises = exSearch.trim() + ? exercises.filter(e => e.name.toLowerCase().includes(exSearch.toLowerCase().trim())) + : exercises; + const handleSaveNewExercise = async (fields) => { setSavingEx(true); try { @@ -112,191 +116,288 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { } }; - const muscleChips = (ids, type) => - (ids || []).slice(0, 4).map(id => ( - - {MUSCLES[id]?.label || id} - - )); + const tabLabels = [ + `Øvelser${!exLoading ? ` (${exercises.length})` : ""}`, + `Maler${!tplLoading ? ` (${templates.length})` : ""}`, + ]; return (
- Bibliotek - setTabIndex(selectedIndex)}> - - Øvelser{!exLoading ? ` (${exercises.length})` : ""} - Maler{!tplLoading ? ` (${templates.length})` : ""} - - + BIBLIOTEK + Dine byggklosser. + + {/* Pill tab strip */} +
{ + if (e.key === "ArrowLeft") setTabIndex(0); + if (e.key === "ArrowRight") setTabIndex(1); + }} + role="tablist" + aria-label="Bibliotek-seksjoner" + > + {tabLabels.map((label, i) => ( + + ))} +
- {/* ── ØVELSER ── */} - - {exError && ( - - )} + {/* ── ØVELSER ── */} + {tabIndex === 0 && ( +
+ {exError && ( + + )} - {!showNewEx && ( - - )} + {!showNewEx && ( + + )} + + {/* Snarvei carousel — template shortcuts */} + {!tplLoading && templates.length > 0 && ( +
+

+ SNARVEIER +

+
+ {templates.map(tpl => { + const exCount = tpl.session_template_exercises?.length || 0; + return ( + + ); + })} +
+
+ )} - {showNewEx && ( - setShowNewEx(false)} - saving={savingEx} - /> - )} + {/* Search */} + {!exLoading && exercises.length > 0 && ( +
+ + setExSearch(e.target.value)} + style={{ + width: "100%", boxSizing: "border-box", + padding: "8px 12px 8px 34px", + background: "var(--surface-card)", + border: "1px solid var(--border-subtle-wl)", + borderRadius: 8, + color: "var(--cds-text-primary)", + fontFamily: "var(--cds-font-sans)", fontSize: 14, + outline: "none", + }} + /> +
+ )} - {exLoading ? ( - - ) : exercises.length === 0 && !showNewEx ? ( -

- Ingen øvelser lagt til ennå. -

- ) : ( -
- {exercises.map(ex => ( -
- {editingEx?.id === ex.id ? ( - handleUpdateExercise(ex.id, fields)} - onCancel={() => setEditingEx(null)} - saving={savingEx} - /> - ) : ( -
-
-
- {ex.name} -
-
- {muscleChips(ex.primary_muscles, "primary")} - {muscleChips(ex.secondary_muscles, "secondary")} - {!(ex.primary_muscles?.length) && !(ex.secondary_muscles?.length) && ( - Ingen muskler - )} -
-
- {(ex.default_sets && ex.default_reps) && ( - - {ex.default_sets}×{ex.default_reps} + {showNewEx && ( + setShowNewEx(false)} + saving={savingEx} + /> + )} + + {exLoading ? ( + + ) : filteredExercises.length === 0 && !showNewEx ? ( +

+ {exSearch.trim() ? "Ingen øvelser matcher søket." : "Ingen øvelser lagt til ennå."} +

+ ) : ( +
+ {filteredExercises.map(ex => ( +
+ {editingEx?.id === ex.id ? ( + handleUpdateExercise(ex.id, fields)} + onCancel={() => setEditingEx(null)} + saving={savingEx} + /> + ) : ( +
+
+
+ {ex.name} +
+
+ {(ex.primary_muscles || []).slice(0, 4).map(id => ( + {MUSCLES[id]?.label || id} + ))} + {(ex.secondary_muscles || []).slice(0, 3).map(id => ( + + {MUSCLES[id]?.label || id} + ))} + {!(ex.primary_muscles?.length) && !(ex.secondary_muscles?.length) && ( + Ingen muskler )} -
+
+ {(ex.default_sets && ex.default_reps) && ( + + {ex.default_sets}×{ex.default_reps} + )} +
- ))} + )}
- )} - + ))} +
+ )} +
+ )} - {/* ── MALER ── */} - - {tplError && ( - - )} + {/* ── MALER ── */} + {tabIndex === 1 && ( +
+ {tplError && ( + + )} - {!showNewTpl && ( - - )} + {!showNewTpl && ( + + )} - {showNewTpl && ( -
- setNewTplName(e.target.value)} - placeholder="f.eks. CrossFit - Anna - mandag" - onKeyDown={(e) => e.key === "Enter" && handleSaveNewTemplate()} - style={{ marginBottom: 12 }} - /> -
- - -
-
- )} + {showNewTpl && ( +
+ setNewTplName(e.target.value)} + placeholder="f.eks. CrossFit - Anna - mandag" + onKeyDown={(e) => e.key === "Enter" && handleSaveNewTemplate()} + style={{ marginBottom: 12 }} + /> +
+ + +
+
+ )} - {tplLoading ? ( - - ) : templates.length === 0 && !showNewTpl ? ( -

- Ingen maler opprettet ennå. -

- ) : ( -
- {templates.map(tpl => { - const exCount = tpl.session_template_exercises?.length || 0; - const usedAt = tpl.used_at - ? new Date(tpl.used_at).toLocaleDateString("no-NO") - : null; - const tplPrimary = [...new Set((tpl.session_template_exercises || []).flatMap(e => e.primary_muscles || []))]; - const muscleCount = tplPrimary.length; - return ( -
-
onEditTemplate(tpl)} - > -
-
-
-
onEditTemplate(tpl)}> -
- {tpl.name} -
-
- {exCount} ØVELSER · {muscleCount} MUSKLER{usedAt ? ` · SIST ${usedAt}` : ""} -
-
-
+ ); + })} +
+ )} +
+ )} +
({ const DAY_HEADERS = ["ma", "ti", "on", "to", "fr", "lø", "sø"]; function calHeatColor(count) { - if (!count) return "var(--cds-background)"; + if (!count) return "var(--surface-card)"; if (count <= 1) return "var(--heat-1)"; if (count <= 3) return "var(--heat-2)"; if (count <= 5) return "var(--heat-3)"; @@ -52,7 +52,7 @@ function MonthGrid({ year, month, sessionCountMap, onDayClick, selectedDate, tod
{cells.map((dateStr, i) => { - if (!dateStr) return
; + if (!dateStr) return
; const count = sessionCountMap[dateStr] || 0; const isToday = dateStr === todayStr; const isSelected = dateStr === selectedStr; @@ -61,10 +61,11 @@ function MonthGrid({ year, month, sessionCountMap, onDayClick, selectedDate, tod const day = parseInt(dateStr.split("-")[2], 10); const cellStyle = { height: 40, + borderRadius: 0, background: calHeatColor(count), - border: "1px solid var(--cds-border-strong-01)", - outline: isSelected ? "2px solid var(--cds-interactive)" : isToday ? "2px solid var(--cds-text-primary)" : undefined, - outlineOffset: "-2px", + border: "1px solid var(--border-subtle-wl)", + outline: isSelected ? "3px solid #ffffff" : isToday ? "1px dashed var(--cds-text-secondary)" : undefined, + outlineOffset: isSelected ? "-3px" : "-2px", display: "flex", alignItems: "center", justifyContent: "center", }; const daySpan = ( @@ -125,6 +126,61 @@ function sessionMuscleIds(session) { ); } +function heroMotivation(count) { + if (count < 1) return null; + if (count === 1) return "god start!"; + if (count === 2) return "to for to!"; + if (count === 3) return "tre på rad!"; + if (count === 4) return "fire! fint."; + if (count === 5) return "fem. solid."; + if (count === 6) return "seks. i rute."; + if (count === 7) return "syv. nesten daglig."; + if (count === 8) return "åtte. kroppen takker."; + if (count === 9) return "ni. ett til!"; + if (count === 10) return "tosifret!"; + if (count === 11) return "elleve. du mener det."; + if (count === 12) return "tolv. tre per uke."; + if (count === 13) return "tretten. heldig kropp."; + if (count === 14) return "fjorten. halvveis til 28."; + if (count === 15) return "femten. meget bra."; + if (count === 16) return "seksten. du er maskinen."; + if (count === 17) return "sytten. ett per muskel!"; + if (count === 18) return "atten. kortet tjener inn."; + if (count === 19) return "nitten. ett til!"; + if (count === 20) return "tjue. dette er en vane."; + if (count === 21) return "tjueen. vanedannende."; + if (count === 22) return "tjueto. ingen stopper deg."; + if (count === 23) return "Jordan-nummer."; + if (count === 24) return "tjuefire. Kobe-territorium."; + if (count === 25) return "kvartmål!"; + if (count === 26) return "tjueseks. halvveis til 52."; + if (count === 27) return "tjuesyv. over Kobe."; + if (count === 28) return "tjueåtte. én per dag?"; + if (count === 29) return "tjueni. nesten 30!"; + if (count === 30) return "tredve. legendarisk."; + if (count === 31) return "trettieen. hver dag."; + if (count === 32) return "Rocky-modus."; + if (count === 33) return "trettire. halvtredjes."; + if (count === 34) return "trettfire. dedikert."; + if (count === 35) return "trettiofem. femgangen!"; + if (count === 36) return "seksgangen squared."; + if (count === 37) return "trettisyv. dette er deg."; + if (count === 38) return "trettåtte. bevisst."; + if (count === 39) return "trettini. nesten firti!"; + if (count === 40) return "FIRTI. Arnold nikker."; + if (count === 41) return "over 40. egen klasse."; + if (count === 42) return "svaret på alt."; + if (count === 43) return "førtitre. hvem gjør det?"; + if (count === 44) return "førtfire. dobbel innsats."; + if (count === 45) return "førtiofem. fire-og-halv timer."; + if (count === 46) return "ikke normalt. kompliment."; + if (count === 47) return "førtisyv. legen er stolt."; + if (count === 48) return "én og en halv per dag."; + if (count === 49) return "ett til: femti-klubben!"; + if (count === 50) return "FEMTI. ikke virkelig."; + return "over 50. ring legen."; +} + export default function History({ initialDate }) { const [sessions, setSessions] = useState([]); @@ -201,6 +257,11 @@ export default function History({ initialDate }) { return map; }, [filteredSessions]); + const currentMonthCount = useMemo(() => filteredSessions.filter(s => { + const d = new Date(s.session_date + "T12:00:00"); + return d.getFullYear() === viewYear && d.getMonth() === viewMonth; + }).length, [filteredSessions, viewYear, viewMonth]); + useEffect(() => { if (daySessions.length === 1) { setExpandedIds(new Set([daySessions[0].id])); @@ -264,10 +325,10 @@ export default function History({ initialDate }) { setEditMode(true); fetchGymSessionsByDate(session.session_date) .then(setEditGymSessions) - .catch(() => setEditGymSessions([])); // gym calendar is optional — edit still works without it + .catch(() => setEditGymSessions([])); fetchLibraryExercises() .then(setLibraryExercises) - .catch(() => {}); // autocomplete degrades silently to manual entry on failure + .catch(() => {}); }; const cancelEdit = () => { @@ -287,7 +348,7 @@ export default function History({ initialDate }) { } checkGymCalendarConflict(editGymSessionId, selectedSession?.id) .then(setEditGymCalendarConflict) - .catch(() => setEditGymCalendarConflict(null)); // treat conflict check failure as no conflict + .catch(() => setEditGymCalendarConflict(null)); }, [editGymSessionId, editMode, selectedSession]); const saveEdit = async () => { @@ -354,352 +415,407 @@ export default function History({ initialDate }) { [editMode, editExercises] ); - const hasEditErrors = editMode && ( editExercises.some(e => e.enabled && !e.name?.trim()) || editExercises.some(e => isInvalidNum(e.sets) || isInvalidNum(e.reps)) ); + const toggleMuscleFilter = (id) => { + const newFilter = muscleFilter.includes(id) + ? muscleFilter.filter(x => x !== id) + : [...muscleFilter, id]; + setMuscleFilter(newFilter); + if (!selectedDate && newFilter.length > 0) { + const matching = sessions.filter(s => newFilter.some(mid => sessionMuscleIds(s).has(mid))); + const todayStr = format(today, "yyyy-MM-dd"); + const dates = matching.map(s => s.session_date).sort(); + const target = dates.includes(todayStr) ? todayStr : dates[dates.length - 1]; + if (target) { + setSelectedDate(new Date(target + "T12:00:00")); + loadSession(target); + } + } + }; + return (
- HISTORIKK - Treningshistorikk - - item?.label ?? ""} - onChange={({ selectedItems }) => setMuscleFilter(selectedItems.map(i => i.id))} - style={{ marginBottom: 16 }} - /> - - {loading ? ( -
- -
+ HISTORIKK + + {muscleFilter.length > 0 && selectedDate ? (() => { + const selectedDateStr = format(selectedDate, "yyyy-MM-dd"); + const count = filteredSessions.filter(s => s.session_date === selectedDateStr).length; + const total = sessions.filter(s => s.session_date === selectedDateStr).length; + const dateLabel = format(selectedDate, "d. MMMM", { locale: nb }); + return <>{count} av {total} {total === 1 ? "økt" : "økter"} den {dateLabel}; + })() : muscleFilter.length > 0 ? ( + <>{currentMonthCount} {currentMonthCount === 1 ? "økt" : "økter"} i {format(new Date(viewYear, viewMonth, 1), "MMMM", { locale: nb })} med disse filtrene ) : ( -
-
-
- -
- VOLUM 1 - {["--heat-1","--heat-2","--heat-3","--heat-4","--heat-5"].map(v => ( -
- ))} - 5+ -
-
+ <>{currentMonthCount} {currentMonthCount === 1 ? "økt" : "økter"} i {format(new Date(viewYear, viewMonth, 1), "MMMM", { locale: nb })}.{heroMotivation(currentMonthCount) && <> {heroMotivation(currentMonthCount)}} )} + + + {/* Muscle filter — horizontal pill scroll */} +
+
+ {MUSCLE_FILTER_ITEMS.map(item => { + const active = muscleFilter.includes(item.id); + return ( + + ); + })} +
+ +
- {loadingSession && ( -
- + {loading ? ( +
+ +
+ ) : ( +
+
+
- )} - - {daySessions.length > 0 && ( -
-

- {format(new Date(daySessions[0].session_date + "T12:00:00"), "EEEE d. MMMM yyyy", { locale: nb })} -

- - {[...daySessions].sort((a, b) => { - if (!muscleFilter.length) return 0; - const aMatch = muscleFilter.some(id => sessionMuscleIds(a).has(id)); - const bMatch = muscleFilter.some(id => sessionMuscleIds(b).has(id)); - return aMatch === bMatch ? 0 : aMatch ? -1 : 1; - }).map((session) => { - const isEditing = editMode && selectedSession?.id === session.id; - const isExpanded = expandedIds.has(session.id); - const sessionMuscles = isEditing ? editMuscles : extractMuscles(session); - const sessionMuscleMap = isEditing ? buildMuscleMapFromExercises(editExercises) : buildMuscleMapFromSession(session); - const exCount = (session.session_exercises || []).filter(e => e.name).length; - const musIds = sessionMuscleIds(session); - const isFilterMatch = muscleFilter.length > 0 && muscleFilter.some(id => musIds.has(id)); - const matchedLabels = isFilterMatch - ? muscleFilter.filter(id => musIds.has(id)).map(id => MUSCLES[id]?.label || id) - : []; - const topMuscles = extractMuscles(session).primary.slice(0, 2).map(id => MUSCLES[id]?.label || id); - const sessionTime = session.gym_calendar?.start_time - ? new Date(session.gym_calendar.start_time).toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }) - : new Date(session.created_at).toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }); - const sessionTitle = session.gym_calendar - ? `${sessionTime} – ${session.gym_calendar.name}` - : `${sessionTime} – Egentrening`; - - return ( -
0 && !isFilterMatch ? 0.45 : 1 }}> - + +
+ VOLUM 1 + {["--heat-1","--heat-2","--heat-3","--heat-4","--heat-5"].map(v => ( +
+ ))} + 5+ +
+
+ )} - {isExpanded && ( -
+ {loadingSession && ( +
+ +
+ )} - {/* Gym class tag (read) or selector (edit) */} - {isEditing ? ( - editGymSessions.length > 0 && ( - <> - - {editGymCalendarConflict && ( - - )} - - ) - ) : ( - session.gym_calendar && ( -
- {session.gym_calendar.name} -
- ) - )} - - {/* Body map */} - - -
- {hoveredMuscle ? ( -
-
- {MUSCLES[hoveredMuscle]?.label} -
-
-
- - {(sessionMuscleMap[hoveredMuscle] || []).length} - - - {(sessionMuscleMap[hoveredMuscle] || []).length === 1 ? "ØVELSE" : "ØVELSER"} - -
- - {(sessionMuscleMap[hoveredMuscle] || []).join(" · ")} - -
-
- ) : ( -
- Hold musepeker over kroppen for detaljer -
- )} -
+ {daySessions.length > 0 && ( +
+

+ {format(new Date(daySessions[0].session_date + "T12:00:00"), "EEEE d. MMMM yyyy", { locale: nb })} +

-
- Primær ({sessionMuscles.primary.length}) - Sekundær ({sessionMuscles.secondary.length}) + {[...daySessions].sort((a, b) => { + if (!muscleFilter.length) return 0; + const aMatch = muscleFilter.some(id => sessionMuscleIds(a).has(id)); + const bMatch = muscleFilter.some(id => sessionMuscleIds(b).has(id)); + return aMatch === bMatch ? 0 : aMatch ? -1 : 1; + }).map((session) => { + const isEditing = editMode && selectedSession?.id === session.id; + const isExpanded = expandedIds.has(session.id); + const sessionMuscles = isEditing ? editMuscles : extractMuscles(session); + const sessionMuscleMap = isEditing ? buildMuscleMapFromExercises(editExercises) : buildMuscleMapFromSession(session); + const exCount = (session.session_exercises || []).filter(e => e.name).length; + const musIds = sessionMuscleIds(session); + const isFilterMatch = muscleFilter.length > 0 && muscleFilter.some(id => musIds.has(id)); + const matchedLabels = isFilterMatch + ? muscleFilter.filter(id => musIds.has(id)).map(id => MUSCLES[id]?.label || id) + : []; + const topMuscles = extractMuscles(session).primary.slice(0, 2).map(id => MUSCLES[id]?.label || id); + const sessionTime = session.gym_calendar?.start_time + ? new Date(session.gym_calendar.start_time).toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }) + : new Date(session.created_at).toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }); + const sessionTitle = session.gym_calendar + ? `${sessionTime} – ${session.gym_calendar.name}` + : `${sessionTime} – Egentrening`; + + return ( +
0 && !isFilterMatch ? 0.45 : 1 }}> + - {/* Exercise list */} -
-

- Øvelser -

+ {isExpanded && ( +
+ {/* Gym class tag (read) or selector (edit) */} {isEditing ? ( - <> -
- {editExercises.map((ex) => ( - setEditExercises(p => p.map(e => e.id === ex.id ? { ...e, ...updates } : e))} - onDelete={() => setEditExercises(p => p.filter(e => e.id !== ex.id))} - layer="layer-02" - validateNumbers - libraryExercises={libraryExercises} - isNew={newExerciseIds.has(ex.id)} + editGymSessions.length > 0 && ( + <> + + {editGymCalendarConflict && ( + - ))} -
- - + )} + + ) ) : ( - (session.session_exercises || []).map(ex => { - const muscleLabels = (ex.muscle_activations || []).map(ma => MUSCLES[ma.muscle_id]?.label || ma.muscle_id).join(", "); - return ( -
- - {muscleLabels ? ( - {ex.name} - ) : ex.name} - - {(ex.sets || ex.reps) && ( - - {[ex.sets && `${ex.sets}×`, ex.reps].filter(Boolean).join("")} - - )} -
- ); - }) + session.gym_calendar && ( +
+ {session.gym_calendar.name} +
+ ) )} -
- {/* Muscle groups (read mode only) */} - {!isEditing && ( -
-

- Muskelgrupper -

- {sessionMuscles.primary.map(id => { - const exNames = (sessionMuscleMap[id] || []).join(", "); - return ( -
-
- - {exNames ? ( - {MUSCLES[id]?.label || id} - ) : MUSCLES[id]?.label || id} - - Primær + {/* Body map */} + + +
+ {hoveredMuscle ? ( +
+
+ {MUSCLES[hoveredMuscle]?.label}
- ); - })} - {sessionMuscles.secondary.map(id => { - const exNames = (sessionMuscleMap[id] || []).join(", "); - return ( -
-
- - {exNames ? ( - {MUSCLES[id]?.label || id} - ) : MUSCLES[id]?.label || id} +
+
+ + {(sessionMuscleMap[hoveredMuscle] || []).length} + + + {(sessionMuscleMap[hoveredMuscle] || []).length === 1 ? "ØVELSE" : "ØVELSER"} + +
+ + {(sessionMuscleMap[hoveredMuscle] || []).join(" · ")} - Sekundær
- ); - })} +
+ ) : ( +
+ Hold musepeker over kroppen for detaljer +
+ )}
- )} - {/* Edit mode actions */} - {isEditing && ( - <> - {analyzeError && ( - - )} - {editError && ( - +
+ Primær ({sessionMuscles.primary.length}) + Sekundær ({sessionMuscles.secondary.length}) +
+ + {/* Exercise list */} +
+

+ Øvelser +

+ + {isEditing ? ( + <> +
+ {editExercises.map((ex) => ( + setEditExercises(p => p.map(e => e.id === ex.id ? { ...e, ...updates } : e))} + onDelete={() => setEditExercises(p => p.filter(e => e.id !== ex.id))} + layer="layer-02" + validateNumbers + libraryExercises={libraryExercises} + isNew={newExerciseIds.has(ex.id)} + /> + ))} +
+ + + ) : ( + (session.session_exercises || []).map(ex => { + const muscleLabels = (ex.muscle_activations || []).map(ma => MUSCLES[ma.muscle_id]?.label || ma.muscle_id).join(", "); + return ( +
+ + {muscleLabels ? ( + {ex.name} + ) : ex.name} + + {(ex.sets || ex.reps) && ( + + {[ex.sets && `${ex.sets}×`, ex.reps].filter(Boolean).join("")} + + )} +
+ ); + }) )} - { if (e.target.files[0]) reanalyze(e.target.files[0]); e.target.value = ""; }} /> -
- - - +
+ + {/* Muscle groups (read mode only) */} + {!isEditing && ( +
+

+ Muskelgrupper +

+ {sessionMuscles.primary.map(id => { + const exNames = (sessionMuscleMap[id] || []).join(", "); + return ( +
+
+ + {exNames ? ( + {MUSCLES[id]?.label || id} + ) : MUSCLES[id]?.label || id} + + Primær +
+ ); + })} + {sessionMuscles.secondary.map(id => { + const exNames = (sessionMuscleMap[id] || []).join(", "); + return ( +
+
+ + {exNames ? ( + {MUSCLES[id]?.label || id} + ) : MUSCLES[id]?.label || id} + + Sekundær +
+ ); + })}
- - )} - - {/* Read mode: edit button (hidden when any session is in edit mode) */} - {!editMode && ( - - )} -
+ )} + + {/* Edit mode actions */} + {isEditing && ( + <> + {analyzeError && ( + + )} + {editError && ( + + )} + { if (e.target.files[0]) reanalyze(e.target.files[0]); e.target.value = ""; }} /> +
+ + + +
+ + )} + + {/* Read mode: edit button (hidden when any session is in edit mode) */} + {!editMode && ( + + )} +
)}
- ); - })} -
- )} + ); + })} +
+ )} - {!loading && sessions.length === 0 && ( -

- Ingen økter lagret ennå. -

- )} + {!loading && sessions.length === 0 && ( +

+ Ingen økter lagret ennå. +

+ )} -
+
); } diff --git a/app/src/components/Report.jsx b/app/src/components/Report.jsx index d229372..262e3d4 100644 --- a/app/src/components/Report.jsx +++ b/app/src/components/Report.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { subDays, format } from "date-fns"; import { nb } from "date-fns/locale"; import { fetchSessionsForReport } from "../lib/db"; @@ -8,8 +8,9 @@ import { CLAUDE_MODEL_TEXT, buildPeriodRecommendPrompt } from "../lib/prompts"; import { Tag, InlineLoading, DefinitionTooltip, Button, InlineNotification, } from "@carbon/react"; -import { AiGenerate } from "@carbon/icons-react"; -import PageShell, { SectionLabel, PageHeading } from "./PageShell"; +import { AiGenerate, Add } from "@carbon/icons-react"; +import PageShell, { SectionLabel, AccentChip, StickyCta } from "./PageShell"; +import { useNav } from "../lib/NavContext"; const PERIODS = [ { label: "7 dager", days: 7 }, @@ -32,17 +33,19 @@ function FilterChip({ label, active, onClick }) { - )} -
-
- {DAYS.map(d => ( - toggleDay(d.day)} - /> - ))} -
+ {/* Filters */} +
+ {/* Row 1: period */} +
+ {PERIODS.map(p => ( + setPeriodDays(p.days)} /> + ))}
- + {/* Row 2: weekdays */} +
+ {DAYS.map(d => ( + toggleDay(d.day)} /> + ))} +
+ {/* Row 3: session types — only when present */} {availableTypes.length > 0 && ( -
-
-

Økttype (tom = alle)

- {selectedTypes.size > 0 && ( - - )} -
-
- {availableTypes.map(name => ( - toggleType(name)} - /> - ))} -
+
+ {availableTypes.map(name => ( + toggleType(name)} /> + ))}
)} + +
-
+
{loading ? ( ) : error ? (

{error}

) : ( <> -
+ {/* KPI tiles */} +
- +
+
+ {musclesCovered} + /17 +
+
Muskler
+
-
+ {/* Heatmap body */} +
{["front", "back"].map(view => ( -
+
))}
+ {/* Hover detail card */}
{hoveredMuscle ? ( -
-
+
+
{MUSCLES[hoveredMuscle]?.label}
@@ -336,24 +354,25 @@ export default function Report() { {muscleCounts[hoveredMuscle]?.primary || 0} - + PRIMÆRØKTER
{muscleLastDate[hoveredMuscle] && ( - + SIST {format(new Date(muscleLastDate[hoveredMuscle] + "T12:00:00"), "d. MMM", { locale: nb })} )}
) : ( -
+
Hold musepeker over eller fokuser muskel for detaljer
)}
+ {/* Heat legend */}
@@ -361,7 +380,7 @@ export default function Report() {
))}
- Primær + Primær
@@ -373,32 +392,36 @@ export default function Report() { - Sekundær + Sekundær
+ {/* Gap callout card */} {untrainedMuscles.length > 0 && ( -
-

- Ikke trent i perioden +

+

+ IKKE TRUFFET

{untrainedMuscles.map(id => ( - {MUSCLES[id]?.label || id} + + {MUSCLES[id]?.label || id} + ))}
)} -
+ {/* Frequency table */} +

Muskelfrekvens

- - + + - + + @@ -408,14 +431,14 @@ export default function Report() { ? "var(--cds-text-primary)" : secondary > 0 ? "#4589ff" - : "var(--cds-text-disabled)"; + : "var(--text-disabled-wl)"; const countLabel = primary > 0 ? String(primary) : secondary > 0 ? `(${secondary})` : "—"; return ( - + - @@ -456,20 +479,18 @@ export default function Report() {
- {loadingRecs && ( - - )} - - {recsError && ( - - )} + {loadingRecs && ( + + )} + {recsError && ( + + )}
{recs && recs.length > 0 && (() => { @@ -478,43 +499,70 @@ export default function Report() { const recSecondary = recSecAll.filter(id => !recPrimary.includes(id)); return (
-
-

Anbefalte øvelser

+
+

Anbefalte øvelser

{recs.map((r, i) => ( -
-

{r.name}

-
- {(r.primary || []).length > 0 && ( - {r.primary.map(id => MUSCLES[id]?.label || id).join(", ")} - )} - {(r.secondary || []).length > 0 && ( - {r.secondary.map(id => MUSCLES[id]?.label || id).join(", ")} - )} +
+
+

{r.name}

+

+ {[ + (r.primary || []).map(id => MUSCLES[id]?.label || id).join(", "), + (r.secondary || []).length > 0 && `(${(r.secondary || []).map(id => MUSCLES[id]?.label || id).join(", ")})` + ].filter(Boolean).join(" · ")} +

+ {r.tip &&

{r.tip}

}
- {r.tip &&

{r.tip}

} +
))}
{isMobile ? ( <> -
+
{["front", "back"].map(v => ( - + ))}
-
+
) : ( -
+
{["front", "back"].map(view => ( -
+
@@ -522,7 +570,7 @@ export default function Report() {
)} -
+
Primær Sekundær
@@ -545,8 +593,26 @@ export default function Report() { )} )} -
+ + {/* Sticky CTA to Bibliotek */} + {recs && recs.length > 0 && ( + + + + )} +
); } diff --git a/app/src/styles/app.css b/app/src/styles/app.css index 841bc82..99652af 100644 --- a/app/src/styles/app.css +++ b/app/src/styles/app.css @@ -1,4 +1,5 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body { overflow-x: hidden; } @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); }
MUSKEL
MUSKEL - ØKTSETTØKTSETT
{muscleExercises[id]?.size > 0 ? ( @@ -424,16 +447,16 @@ export default function Report() { ) : MUSCLES[id]?.label || id} -
+
{primary > 0 && ( -
+
)}
{countLabel} + {muscleVolume[id] > 0 ? muscleVolume[id] : "—"}