From a4724e7118ee0afd8fae145f0f28d921492a2798 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 1 Feb 2026 11:09:16 +0100 Subject: [PATCH] Add Dashboard with weekly calendar and today's workout - Dashboard.jsx: main landing page after login - Coach greeting based on time of day - Weekly calendar showing workout days - Today's workout card with exercises - Quick stats (workouts/week, streak) - Upcoming workouts list - Full responsive CSS - App.jsx updated to show Dashboard first --- frontend/src/App.css | 398 +++++++++++++++++++++++++++++++ frontend/src/App.jsx | 84 ++----- frontend/src/pages/Dashboard.jsx | 246 +++++++++++++++++++ 3 files changed, 659 insertions(+), 69 deletions(-) create mode 100644 frontend/src/pages/Dashboard.jsx diff --git a/frontend/src/App.css b/frontend/src/App.css index 32b7e78..4eee35c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -375,3 +375,401 @@ padding-bottom: calc(1rem + env(safe-area-inset-bottom)); } } + +/* ============================================ + DASHBOARD STYLES + ============================================ */ + +.dashboard { + min-height: 100vh; + background: var(--bg); +} + +.dashboard.loading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1rem; +} + +/* Dashboard Header */ +.dashboard-header { + background: var(--bg-secondary); + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; +} + +.header-top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-top h1 { + font-size: 1.5rem; + font-weight: 700; +} + +.nav-menu { + display: flex; + gap: 0.25rem; +} + +.nav-btn { + background: transparent; + border: none; + color: var(--text-muted); + padding: 0.5rem 0.75rem; + border-radius: 8px; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s; +} + +.nav-btn:hover, +.nav-btn.active { + background: var(--bg); + color: var(--text); +} + +.nav-btn.logout { + color: var(--text-muted); +} + +/* Dashboard Main */ +.dashboard-main { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 600px; + margin: 0 auto; +} + +/* Coach Greeting */ +.coach-greeting { + display: flex; + gap: 1rem; + padding: 1rem; + background: linear-gradient(135deg, var(--accent) 0%, #6366f1 100%); + border-radius: 16px; + color: white; +} + +.coach-avatar { + font-size: 2.5rem; + width: 60px; + height: 60px; + background: rgba(255,255,255,0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.coach-message { + display: flex; + align-items: center; +} + +.coach-message p { + font-size: 1.1rem; + font-weight: 500; + line-height: 1.4; +} + +/* Week Calendar */ +.week-calendar { + background: var(--bg-secondary); + border-radius: 16px; + padding: 1rem; + border: 1px solid var(--border); +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.calendar-title { + font-weight: 600; + text-transform: capitalize; +} + +.calendar-nav { + background: var(--bg); + border: 1px solid var(--border); + width: 32px; + height: 32px; + border-radius: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--text); + transition: all 0.2s; +} + +.calendar-nav:hover { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +.calendar-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0.5rem; +} + +.calendar-day { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.5rem; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; + position: relative; +} + +.calendar-day:hover { + background: var(--bg); +} + +.calendar-day.today { + background: var(--accent); + color: white; +} + +.calendar-day.has-workout .day-dot { + color: var(--success); +} + +.calendar-day.today .day-dot { + color: white; +} + +.day-name { + font-size: 0.7rem; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.calendar-day.today .day-name { + color: rgba(255,255,255,0.8); +} + +.day-date { + font-size: 1rem; + font-weight: 600; +} + +.day-dot { + font-size: 0.5rem; + margin-top: 0.25rem; +} + +/* Today's Workout */ +.todays-workout { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.todays-workout h2 { + font-size: 1.1rem; + font-weight: 600; +} + +.workout-card { + background: var(--bg-secondary); + border-radius: 16px; + padding: 1.25rem; + border: 1px solid var(--border); + cursor: pointer; + transition: all 0.2s; +} + +.workout-card:hover { + border-color: var(--accent); + transform: translateY(-2px); +} + +.workout-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.workout-card-header h3 { + font-size: 1.2rem; + font-weight: 600; +} + +.workout-duration { + font-size: 0.85rem; + color: var(--text-muted); +} + +.workout-exercises { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.exercise-preview { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); +} + +.exercise-preview:last-child { + border-bottom: none; +} + +.exercise-name { + font-weight: 500; +} + +.exercise-sets { + color: var(--text-muted); + font-size: 0.9rem; +} + +.start-workout-btn { + width: 100%; + background: var(--accent); + color: white; + border: none; + padding: 1rem; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.start-workout-btn:hover { + background: var(--accent-hover); + transform: scale(1.02); +} + +/* Rest Day Card */ +.rest-day-card { + background: var(--bg-secondary); + border-radius: 16px; + padding: 2rem; + border: 1px solid var(--border); + text-align: center; +} + +.rest-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.rest-day-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; +} + +.rest-day-card p { + color: var(--text-muted); + margin-bottom: 1rem; +} + +.rest-tips { + display: flex; + justify-content: center; + gap: 1rem; +} + +.rest-tips span { + background: var(--bg); + padding: 0.5rem 0.75rem; + border-radius: 20px; + font-size: 0.85rem; +} + +/* Quick Stats */ +.quick-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} + +.stat-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 1rem; + text-align: center; + border: 1px solid var(--border); +} + +.stat-value { + display: block; + font-size: 1.5rem; + font-weight: 700; + color: var(--accent); +} + +.stat-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; +} + +/* Upcoming Workouts */ +.upcoming-workouts h2 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.75rem; +} + +.upcoming-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.upcoming-item { + display: flex; + align-items: center; + gap: 1rem; + background: var(--bg-secondary); + padding: 1rem; + border-radius: 12px; + border: 1px solid var(--border); + cursor: pointer; + transition: all 0.2s; +} + +.upcoming-item:hover { + border-color: var(--accent); +} + +.upcoming-day { + font-weight: 600; + width: 40px; + color: var(--accent); +} + +.upcoming-name { + flex: 1; +} + +.upcoming-arrow { + color: var(--text-muted); +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 64f62cc..a7f01db 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,34 +1,30 @@ import { useState, useEffect } from 'react' import { useAuth } from './context/AuthContext' +import Dashboard from './pages/Dashboard' import './App.css' const API_URL = '/api' function App() { const { user, logout } = useAuth() - const [view, setView] = useState('program') + const [view, setView] = useState('dashboard') const [program, setProgram] = useState(null) const [selectedDay, setSelectedDay] = useState(null) const [currentWeek, setCurrentWeek] = useState(1) const [logs, setLogs] = useState({}) - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(false) const userId = user?.id || 1 const today = new Date().toISOString().split('T')[0] - useEffect(() => { - fetchProgram() - }, []) - const fetchProgram = async () => { + if (program) return // Already loaded try { const res = await fetch(`${API_URL}/programs/1`) const data = await res.json() setProgram(data) - setLoading(false) } catch (err) { console.error('Failed to fetch program:', err) - setLoading(false) } } @@ -90,21 +86,19 @@ function App() { } } - const startWorkout = (day) => { + const startWorkout = async (day) => { + await fetchProgram() // Ensure program is loaded setSelectedDay(day) setView('workout') fetchLogs(day.id) } - if (loading) { - return ( -
-
-

Laddar program...

-
- ) + // Dashboard view (default after login) + if (view === 'dashboard') { + return } + // Workout view if (view === 'workout' && selectedDay) { return ( setView('program')} + onBack={() => setView('dashboard')} fetchProgression={fetchProgression} /> ) } + // Fallback loading return ( -
-
-
-

đŸ‹ïž Gravl

- -
-
- - Vecka {currentWeek} - -
-
- -
-
-

{program?.name || 'Laddar...'}

-

{program?.description}

-
- -
-

Veckans pass

- {program?.days?.map((day, idx) => ( -
startWorkout(day)}> -
- Dag {day.day_number} - {day.name} -
-
- {day.exercises?.filter(e => e.name).slice(0, 3).map((ex, i) => ( - {ex.name} - ))} - {day.exercises?.filter(e => e.name).length > 3 && ( - +{day.exercises.filter(e => e.name).length - 3} - )} -
-
- Starta → -
-
- ))} -
-
+
+
+

Laddar...

) } diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..550796c --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../context/AuthContext' + +const API_URL = '/api' + +// Coach greetings based on time and context +const getCoachGreeting = (user, todayWorkout) => { + const hour = new Date().getHours() + const name = user?.name?.split(' ')[0] || 'du' + + if (hour < 10) { + return todayWorkout + ? `Godmorgon ${name}! đŸ’Ș Redo för ${todayWorkout.name.toLowerCase()}?` + : `Godmorgon ${name}! Vilodag idag – Ă„terhĂ€mtning Ă€r ocksĂ„ trĂ€ning.` + } else if (hour < 14) { + return todayWorkout + ? `Dags att köra ${name}! ${todayWorkout.name} vĂ€ntar.` + : `Lugn dag idag ${name}. Ladda batterierna! 🔋` + } else if (hour < 18) { + return todayWorkout + ? `Eftermiddagspass? ${todayWorkout.name} stĂ„r pĂ„ schemat đŸ‹ïž` + : `Vila upp dig ${name}. Imorgon kör vi igen!` + } else { + return todayWorkout + ? `KvĂ€llspass ${name}? Perfekt för att slĂ€ppa dagen.` + : `Bra jobbat denna veckan! Vila gott. 😮` + } +} + +// Get weekday names +const weekdays = ['MĂ„n', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön'] + +function Dashboard({ onStartWorkout }) { + const { user, logout } = useAuth() + const [program, setProgram] = useState(null) + const [todayWorkout, setTodayWorkout] = useState(null) + const [loading, setLoading] = useState(true) + const [currentWeekStart, setCurrentWeekStart] = useState(getWeekStart(new Date())) + + useEffect(() => { + fetchData() + }, []) + + const fetchData = async () => { + try { + // Fetch user's program + const res = await fetch(`${API_URL}/programs/1`) + const data = await res.json() + setProgram(data) + + // Determine today's workout based on day of week + const dayOfWeek = new Date().getDay() // 0 = Sunday + const adjustedDay = dayOfWeek === 0 ? 7 : dayOfWeek // Convert to 1-7 (Mon-Sun) + + // Find if there's a workout scheduled for today + const todayDay = data.days?.find(d => d.day_number === adjustedDay) + setTodayWorkout(todayDay || null) + + setLoading(false) + } catch (err) { + console.error('Failed to fetch data:', err) + setLoading(false) + } + } + + if (loading) { + return ( +
+
+

Laddar...

+
+ ) + } + + const workoutDays = program?.days?.map(d => d.day_number) || [] + + return ( +
+
+
+

đŸ‹ïž Gravl

+ +
+
+ +
+ {/* Coach Greeting */} +
+
đŸ§”â€â™‚ïž
+
+

{getCoachGreeting(user, todayWorkout)}

+
+
+ + {/* Week Calendar */} +
+
+ + + {formatWeekRange(currentWeekStart)} + + +
+
+ {weekdays.map((name, idx) => { + const date = addDays(currentWeekStart, idx) + const dayNum = idx + 1 + const isToday = isSameDay(date, new Date()) + const hasWorkout = workoutDays.includes(dayNum) + const workout = program?.days?.find(d => d.day_number === dayNum) + + return ( +
hasWorkout && workout && onStartWorkout(workout)} + > + {name} + {date.getDate()} + {hasWorkout && ●} +
+ ) + })} +
+
+ + {/* Today's Workout */} +
+

Dagens pass

+ {todayWorkout ? ( +
onStartWorkout(todayWorkout)}> +
+

{todayWorkout.name}

+ ~45 min +
+
+ {todayWorkout.exercises?.filter(e => e.name).map((ex, i) => ( +
+ {ex.name} + {ex.sets}×{ex.reps_min}-{ex.reps_max} +
+ ))} +
+ +
+ ) : ( +
+
🧘
+

Vilodag

+

Inga pass schemalagda. Fokusera pÄ ÄterhÀmtning!

+
+ đŸ’€ Sömn + đŸ„— NĂ€ring + đŸš¶ LĂ€tt rörelse +
+
+ )} +
+ + {/* Quick Stats */} +
+
+ {workoutDays.length} + Pass/vecka +
+
+ 2 + Denna vecka +
+
+ đŸ’Ș + Streak: 5 +
+
+ + {/* Upcoming Workouts */} +
+

Kommande pass

+
+ {program?.days?.slice(0, 3).map((day, idx) => ( +
onStartWorkout(day)} + > + {weekdays[day.day_number - 1]} + {day.name} + → +
+ ))} +
+
+
+
+ ) +} + +// Helper functions +function getWeekStart(date) { + const d = new Date(date) + const day = d.getDay() + const diff = d.getDate() - day + (day === 0 ? -6 : 1) + return new Date(d.setDate(diff)) +} + +function addDays(date, days) { + const d = new Date(date) + d.setDate(d.getDate() + days) + return d +} + +function isSameDay(d1, d2) { + return d1.getDate() === d2.getDate() && + d1.getMonth() === d2.getMonth() && + d1.getFullYear() === d2.getFullYear() +} + +function formatWeekRange(weekStart) { + const end = addDays(weekStart, 6) + const startMonth = weekStart.toLocaleDateString('sv-SE', { month: 'short' }) + const endMonth = end.toLocaleDateString('sv-SE', { month: 'short' }) + + if (startMonth === endMonth) { + return `${weekStart.getDate()} - ${end.getDate()} ${startMonth}` + } + return `${weekStart.getDate()} ${startMonth} - ${end.getDate()} ${endMonth}` +} + +export default Dashboard