Files
gravl/frontend/src/pages/Dashboard.jsx
T
clawd 718b210a14 feat(dashboard): polish header logo, stat cards, calendar and animations
- Replace gravl icon text with Logo component in dashboard header
- Stat cards: gradient depth + per-card colour accent (orange/green/amber)
- Calendar today: pulsing glow animation; workout days get subtle brand tint
- Arrow nudge animation on today-workout-card hover
- Section stagger fade-in on page load (calendar → coach → stats)
- Larger stat-value font (3xl) with tighter letter-spacing
- Consistent gap spacing in dashboard-main (space-6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:22:34 +01:00

251 lines
8.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useAuth } from '../context/AuthContext'
import { Icon, getActivityIconName } from '../components/Icons'
import Logo from '../components/Logo'
const API_URL = '/api'
// Coach greetings based on context
const getCoachGreeting = (user, todayWorkout) => {
const hour = new Date().getHours()
const name = user?.name?.split(' ')[0] || 'du'
if (todayWorkout) {
// There's a workout today
if (hour < 10) {
return `Godmorgon ${name}! Idag kör vi ${todayWorkout.name.toLowerCase()}. Redo?`
} else if (hour < 14) {
return `${todayWorkout.name} står på schemat idag. Dags att köra!`
} else if (hour < 18) {
return `Eftermiddagspass? ${todayWorkout.name} väntar på dig.`
} else {
return `Kvällspass ${name}? ${todayWorkout.name} perfekt för att avsluta dagen.`
}
} else {
// Rest day
if (hour < 10) {
return `Godmorgon ${name}! Vilodag idag perfekt för återhämtning.`
} else if (hour < 14) {
return `Ingen träning schemalagd. Ta en promenad eller stretcha lite?`
} else if (hour < 18) {
return `Vila är också träning! Lätt rörelse eller mobilitet idag?`
} else {
return `Lugn kväll ${name}. Ladda batterierna till nästa pass!`
}
}
}
// Rest day tips
const restDayTips = [
{ iconName: 'walking', text: 'Promenad' },
{ iconName: 'yoga', text: 'Stretching' },
{ iconName: 'swimming', text: 'Simning' },
{ iconName: 'cycling', text: 'Cykling' },
]
// Get weekday names
const weekdays = ['Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön']
function Dashboard({ onStartWorkout, onNavigate }) {
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 {
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()
const adjustedDay = dayOfWeek === 0 ? 7 : dayOfWeek
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 (
<div className="dashboard loading">
<div className="spinner"></div>
<p>Laddar...</p>
</div>
)
}
const workoutDays = program?.days?.map(d => d.day_number) || []
return (
<div className="dashboard">
<header className="dashboard-header">
<div className="header-top">
<h1 className="brand-title">
<Logo />
<span className="brand-name">Gravl</span>
</h1>
<nav className="nav-menu">
<button className="nav-btn active"><Icon name="home" size={18} /></button>
<button className="nav-btn" onClick={() => onNavigate('progress')}><Icon name="chart" size={18} /></button>
<button className="nav-btn" onClick={() => onNavigate('profile')}><Icon name="user" size={18} /></button>
<button className="nav-btn logout" onClick={logout}><Icon name="logout" size={18} /></button>
</nav>
</div>
</header>
<main className="dashboard-main">
{/* Week Calendar - TOP */}
<section className="week-calendar">
<div className="calendar-header">
<button
className="calendar-nav"
onClick={() => setCurrentWeekStart(addDays(currentWeekStart, -7))}
>
<Icon name="chevronLeft" size={16} />
</button>
<span className="calendar-title">
{formatWeekRange(currentWeekStart)}
</span>
<button
className="calendar-nav"
onClick={() => setCurrentWeekStart(addDays(currentWeekStart, 7))}
>
<Icon name="chevronRight" size={16} />
</button>
</div>
<div className="calendar-days">
{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 (
<div
key={idx}
className={`calendar-day ${isToday ? 'today' : ''} ${hasWorkout ? 'has-workout' : ''}`}
onClick={() => hasWorkout && workout && onStartWorkout(workout)}
>
<span className="day-name">{name}</span>
<span className="day-date">{date.getDate()}</span>
{hasWorkout && <span className="day-dot" />}
</div>
)
})}
</div>
</section>
{/* Coach Section with Today's Action */}
<section className="coach-section">
<div className="coach-greeting">
<div className="coach-avatar">
<Icon name="coach" size={36} />
</div>
<div className="coach-message">
<p>{getCoachGreeting(user, todayWorkout)}</p>
</div>
</div>
{/* Today's Action */}
<div className="today-action">
{todayWorkout ? (
// Workout today - show workout card
<div className="today-workout-card" onClick={() => onStartWorkout(todayWorkout)}>
<div className="workout-info">
<h3>{todayWorkout.name}</h3>
<span className="workout-meta">
{todayWorkout.exercises?.filter(e => e.name).length} övningar ~45 min
</span>
</div>
<div className="workout-action">
<Icon name="arrowRight" size={24} />
</div>
</div>
) : (
// Rest day - show tips + add button
<div className="rest-day-section">
<div className="rest-tips">
{restDayTips.map((tip, i) => (
<span key={i} className="tip-badge">
<Icon name={tip.iconName} size={16} />
{tip.text}
</span>
))}
</div>
<button
className="add-workout-btn"
onClick={() => onNavigate('select-workout')}
>
<Icon name="plus" size={20} />
<span>Lägg till pass</span>
</button>
</div>
)}
</div>
</section>
{/* Quick Stats */}
<section className="quick-stats">
<div className="stat-card">
<span className="stat-value">{workoutDays.length}</span>
<span className="stat-label">Pass/vecka</span>
</div>
<div className="stat-card">
<span className="stat-value">2</span>
<span className="stat-label">Denna vecka</span>
</div>
<div className="stat-card">
<span className="stat-value stat-icon"><Icon name="fire" size={28} /></span>
<span className="stat-label">Streak: 5</span>
</div>
</section>
</main>
</div>
)
}
// 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