718b210a14
- 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>
251 lines
8.5 KiB
React
251 lines
8.5 KiB
React
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
|