1f2a892391
- New design system: Stitch (kinetic-precision.css) with lime (#cafd00) accent - New Google Fonts: Lexend, Plus Jakarta Sans, Space Grotesk - New page: BenchmarksPage with strength/endurance/body tracking - Redesigned: Dashboard, ProgressPage, WorkoutPage, LoginPage + LoginPage.css - Add shared glassmorphism nav, kinetic buttons, intensity indicators - Build: 265KB JS / 88KB CSS / 2.54s (clean)
664 lines
24 KiB
React
664 lines
24 KiB
React
import { useState, useEffect } from 'react'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import { Icon, getActivityIconName } from '../components/Icons'
|
||
import Logo from '../components/Logo'
|
||
import '../styles/kinetic-precision.css'
|
||
|
||
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) {
|
||
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 {
|
||
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!`
|
||
}
|
||
}
|
||
}
|
||
|
||
const restDayTips = [
|
||
{ iconName: 'walking', text: 'Promenad' },
|
||
{ iconName: 'yoga', text: 'Stretching' },
|
||
{ iconName: 'swimming', text: 'Simning' },
|
||
{ iconName: 'cycling', text: 'Cykling' },
|
||
]
|
||
|
||
const weekdays = ['Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön']
|
||
|
||
// Format volume number
|
||
function formatVolume(kg) {
|
||
if (kg >= 1000) return `${(kg / 1000).toFixed(1).replace('.0', '')} 000`
|
||
return `${kg}`
|
||
}
|
||
|
||
// Format session date
|
||
function formatSessionDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const d = new Date(dateStr)
|
||
return d.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' })
|
||
}
|
||
|
||
// Placeholder recent sessions
|
||
const PLACEHOLDER_SESSIONS = [
|
||
{ id: 1, name: 'Bröst & Triceps', date: new Date(Date.now() - 2 * 86400000).toISOString(), duration: 52, exercise_count: 6, volume: 8750, is_pr: true },
|
||
{ id: 2, name: 'Rygg & Biceps', date: new Date(Date.now() - 4 * 86400000).toISOString(), duration: 48, exercise_count: 7, volume: 11200, is_pr: false },
|
||
{ id: 3, name: 'Ben & Axlar', date: new Date(Date.now() - 6 * 86400000).toISOString(), duration: 61, exercise_count: 8, volume: 14300, is_pr: false },
|
||
]
|
||
|
||
const PLACEHOLDER_MONTHLY = {
|
||
stronger_pct: 15,
|
||
streak: 14,
|
||
total_volume: 124500,
|
||
}
|
||
|
||
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()))
|
||
const [recentSessions, setRecentSessions] = useState(PLACEHOLDER_SESSIONS)
|
||
const [monthlyStats, setMonthlyStats] = useState(PLACEHOLDER_MONTHLY)
|
||
|
||
useEffect(() => {
|
||
fetchData()
|
||
}, [])
|
||
|
||
const fetchData = async () => {
|
||
try {
|
||
const res = await fetch(`${API_URL}/programs/1`)
|
||
const data = await res.json()
|
||
setProgram(data)
|
||
|
||
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)
|
||
}
|
||
|
||
// Fetch workout history (graceful fallback)
|
||
try {
|
||
const histRes = await fetch(`${API_URL}/user/workout-history?user_id=1&limit=5`)
|
||
if (histRes.ok) {
|
||
const histData = await histRes.json()
|
||
if (Array.isArray(histData) && histData.length > 0) {
|
||
setRecentSessions(histData.slice(0, 4))
|
||
// Calculate monthly stats from history
|
||
const now = new Date()
|
||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||
const monthSessions = histData.filter(s => new Date(s.date) >= monthStart)
|
||
const totalVol = monthSessions.reduce((sum, s) => sum + (s.volume || 0), 0)
|
||
setMonthlyStats(prev => ({ ...prev, total_volume: totalVol || prev.total_volume }))
|
||
}
|
||
}
|
||
} catch (_) {
|
||
// use placeholder data
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0e0e0e' }}>
|
||
<div className="spinner"></div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const workoutDays = program?.days?.map(d => d.day_number) || []
|
||
|
||
return (
|
||
<div style={{ minHeight: '100vh', background: '#0e0e0e', paddingBottom: '80px' }}>
|
||
{/* TOP HEADER */}
|
||
<header style={{
|
||
background: '#0e0e0e',
|
||
padding: '1rem 1.25rem 0.75rem',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
position: 'sticky',
|
||
top: 0,
|
||
zIndex: 50,
|
||
}}>
|
||
<span style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 800,
|
||
fontSize: '1.25rem',
|
||
letterSpacing: '0.12em',
|
||
color: '#cafd00',
|
||
textTransform: 'uppercase',
|
||
}}>KINETIC</span>
|
||
<button
|
||
onClick={() => onNavigate('profile')}
|
||
style={{
|
||
width: 38, height: 38,
|
||
borderRadius: '50%',
|
||
background: '#1a1a1a',
|
||
border: '1px solid #262626',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
cursor: 'pointer',
|
||
color: '#adaaaa',
|
||
}}
|
||
>
|
||
<Icon name="user" size={18} />
|
||
</button>
|
||
</header>
|
||
|
||
<main style={{ padding: '0 1.25rem' }}>
|
||
{/* MONTHLY HERO */}
|
||
<section style={{ marginTop: '1.25rem', marginBottom: '1.5rem' }}>
|
||
<div style={{
|
||
background: '#131313',
|
||
borderRadius: '12px',
|
||
padding: '1.5rem 1.25rem 1.25rem',
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
}}>
|
||
{/* Subtle lime glow top-right */}
|
||
<div style={{
|
||
position: 'absolute', top: 0, right: 0,
|
||
width: 120, height: 120,
|
||
background: 'radial-gradient(circle at top right, rgba(202,253,0,0.08), transparent 70%)',
|
||
pointerEvents: 'none',
|
||
}} />
|
||
|
||
<div style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 800,
|
||
fontSize: '1.4rem',
|
||
lineHeight: 1.15,
|
||
color: '#ffffff',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.02em',
|
||
marginBottom: '1rem',
|
||
}}>
|
||
<span style={{ color: '#cafd00' }}>{monthlyStats.stronger_pct}%</span>{' '}
|
||
STARKARE ÄN{' '}
|
||
<span style={{ color: '#adaaaa', fontWeight: 600 }}>FÖRRA MÅNADEN</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||
{/* Streak badge */}
|
||
<div style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: '0.375rem',
|
||
padding: '0.35rem 0.75rem',
|
||
background: 'rgba(202,253,0,0.1)',
|
||
borderRadius: '6px',
|
||
border: '1px solid rgba(202,253,0,0.2)',
|
||
}}>
|
||
<Icon name="fire" size={14} />
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
fontWeight: 700,
|
||
color: '#cafd00',
|
||
letterSpacing: '0.04em',
|
||
textTransform: 'uppercase',
|
||
}}>{monthlyStats.streak} DAGARS STREAK</span>
|
||
</div>
|
||
|
||
{/* Volume */}
|
||
<div>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.65rem',
|
||
color: '#767575',
|
||
letterSpacing: '0.06em',
|
||
textTransform: 'uppercase',
|
||
display: 'block',
|
||
}}>Denna månad</span>
|
||
<span style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '1rem',
|
||
color: '#ffffff',
|
||
}}>{formatVolume(monthlyStats.total_volume)} <span style={{ color: '#767575', fontSize: '0.75rem', fontFamily: 'Space Grotesk' }}>KG</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* WEEK CALENDAR */}
|
||
<section style={{
|
||
background: '#1a1a1a',
|
||
borderRadius: '10px',
|
||
padding: '0.875rem',
|
||
marginBottom: '1.5rem',
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||
<button
|
||
onClick={() => setCurrentWeekStart(addDays(currentWeekStart, -7))}
|
||
style={{ background: 'none', border: 'none', color: '#adaaaa', cursor: 'pointer', padding: '0.25rem' }}
|
||
>
|
||
<Icon name="chevronLeft" size={16} />
|
||
</button>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
color: '#adaaaa',
|
||
letterSpacing: '0.04em',
|
||
textTransform: 'uppercase',
|
||
}}>
|
||
{formatWeekRange(currentWeekStart)}
|
||
</span>
|
||
<button
|
||
onClick={() => setCurrentWeekStart(addDays(currentWeekStart, 7))}
|
||
style={{ background: 'none', border: 'none', color: '#adaaaa', cursor: 'pointer', padding: '0.25rem' }}
|
||
>
|
||
<Icon name="chevronRight" size={16} />
|
||
</button>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '0.25rem' }}>
|
||
{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}
|
||
onClick={() => hasWorkout && workout && onStartWorkout(workout)}
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
gap: '0.25rem',
|
||
padding: '0.5rem 0.25rem',
|
||
borderRadius: '8px',
|
||
background: isToday ? 'rgba(202,253,0,0.1)' : 'transparent',
|
||
border: isToday ? '1px solid rgba(202,253,0,0.25)' : '1px solid transparent',
|
||
cursor: hasWorkout ? 'pointer' : 'default',
|
||
}}
|
||
>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.65rem',
|
||
color: isToday ? '#cafd00' : '#767575',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.04em',
|
||
}}>{name}</span>
|
||
<span style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: isToday ? 700 : 500,
|
||
fontSize: '0.9rem',
|
||
color: isToday ? '#cafd00' : '#ffffff',
|
||
}}>{date.getDate()}</span>
|
||
{hasWorkout && (
|
||
<span style={{
|
||
width: 4, height: 4, borderRadius: '50%',
|
||
background: isToday ? '#cafd00' : '#adaaaa',
|
||
}} />
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
{/* COACH GREETING */}
|
||
<section style={{ marginBottom: '1.25rem' }}>
|
||
<div style={{
|
||
display: 'flex',
|
||
gap: '0.875rem',
|
||
alignItems: 'flex-start',
|
||
}}>
|
||
<div style={{
|
||
width: 40, height: 40,
|
||
borderRadius: '50%',
|
||
background: '#1a1a1a',
|
||
border: '1px solid #262626',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
flexShrink: 0,
|
||
color: '#cafd00',
|
||
}}>
|
||
<Icon name="coach" size={22} />
|
||
</div>
|
||
<div style={{
|
||
background: '#1a1a1a',
|
||
borderRadius: '10px',
|
||
padding: '0.75rem 1rem',
|
||
flex: 1,
|
||
}}>
|
||
<p style={{
|
||
fontFamily: 'Plus Jakarta Sans, sans-serif',
|
||
fontSize: '0.875rem',
|
||
color: '#adaaaa',
|
||
lineHeight: 1.5,
|
||
}}>{getCoachGreeting(user, todayWorkout)}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* TODAY'S WORKOUT CARD */}
|
||
<section style={{ marginBottom: '1.75rem' }}>
|
||
{todayWorkout ? (
|
||
<div
|
||
onClick={() => onStartWorkout(todayWorkout)}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #1a1a1a 0%, #131313 100%)',
|
||
border: '1px solid rgba(202,253,0,0.15)',
|
||
borderRadius: '12px',
|
||
padding: '1.25rem',
|
||
cursor: 'pointer',
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{/* Accent bar */}
|
||
<div style={{
|
||
position: 'absolute', top: 0, left: 0, right: 0,
|
||
height: 3,
|
||
background: 'linear-gradient(90deg, #cafd00, transparent)',
|
||
}} />
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1rem' }}>
|
||
<div>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.65rem',
|
||
color: '#cafd00',
|
||
letterSpacing: '0.08em',
|
||
textTransform: 'uppercase',
|
||
display: 'block',
|
||
marginBottom: '0.25rem',
|
||
}}>Dagens pass</span>
|
||
<h3 style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '1.25rem',
|
||
color: '#ffffff',
|
||
}}>{todayWorkout.name}</h3>
|
||
</div>
|
||
<div style={{
|
||
width: 36, height: 36,
|
||
borderRadius: '8px',
|
||
background: '#cafd00',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: '#516700',
|
||
}}>
|
||
<Icon name="arrowRight" size={18} />
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.25rem' }}>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
color: '#adaaaa',
|
||
letterSpacing: '0.03em',
|
||
}}>
|
||
{todayWorkout.exercises?.filter(e => e.name).length || 0} övningar
|
||
</span>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
color: '#adaaaa',
|
||
letterSpacing: '0.03em',
|
||
}}>~45 min</span>
|
||
</div>
|
||
|
||
<button className="btn-kinetic" style={{ width: '100%', fontSize: '0.875rem', padding: '0.875rem' }}>
|
||
STARTA PASS
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
background: '#1a1a1a',
|
||
borderRadius: '12px',
|
||
padding: '1.25rem',
|
||
}}>
|
||
<h3 style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 600,
|
||
fontSize: '1rem',
|
||
color: '#ffffff',
|
||
marginBottom: '0.875rem',
|
||
}}>Vilodag</h3>
|
||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
||
{restDayTips.map((tip, i) => (
|
||
<span key={i} style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: '0.375rem',
|
||
padding: '0.35rem 0.75rem',
|
||
background: '#131313',
|
||
borderRadius: '6px',
|
||
border: '1px solid #262626',
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
color: '#adaaaa',
|
||
}}>
|
||
<Icon name={tip.iconName} size={14} />
|
||
{tip.text}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<button
|
||
onClick={() => onNavigate('select-workout')}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.75rem',
|
||
background: '#131313',
|
||
border: '1px solid #262626',
|
||
borderRadius: '8px',
|
||
color: '#adaaaa',
|
||
fontFamily: 'Plus Jakarta Sans, sans-serif',
|
||
fontSize: '0.875rem',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '0.5rem',
|
||
}}
|
||
>
|
||
<Icon name="plus" size={16} />
|
||
Lägg till pass
|
||
</button>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{/* RECENT SESSIONS */}
|
||
<section style={{ marginBottom: '2rem' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.875rem' }}>
|
||
<h2 style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '0.875rem',
|
||
color: '#ffffff',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.06em',
|
||
}}>Senaste pass</h2>
|
||
<button
|
||
onClick={() => onNavigate('progress')}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.75rem',
|
||
color: '#cafd00',
|
||
cursor: 'pointer',
|
||
letterSpacing: '0.03em',
|
||
}}
|
||
>Se alla →</button>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.625rem' }}>
|
||
{recentSessions.map((session) => (
|
||
<div
|
||
key={session.id}
|
||
className={session.is_pr ? 'intensity-bar-orange' : 'intensity-bar-lime'}
|
||
style={{
|
||
background: '#1a1a1a',
|
||
borderRadius: '10px',
|
||
padding: '0.875rem 0.875rem 0.875rem 1.25rem',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||
<span style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '0.9375rem',
|
||
color: '#ffffff',
|
||
}}>{session.name}</span>
|
||
{session.is_pr && (
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.65rem',
|
||
fontWeight: 700,
|
||
color: '#516700',
|
||
background: '#cafd00',
|
||
padding: '0.125rem 0.375rem',
|
||
borderRadius: '4px',
|
||
letterSpacing: '0.04em',
|
||
}}>PR</span>
|
||
)}
|
||
</div>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.72rem',
|
||
color: '#767575',
|
||
letterSpacing: '0.03em',
|
||
}}>
|
||
{formatSessionDate(session.date)} · {session.duration} min · {session.exercise_count} övningar
|
||
</span>
|
||
</div>
|
||
<div style={{ textAlign: 'right' }}>
|
||
<span style={{
|
||
fontFamily: 'Lexend, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '0.9rem',
|
||
color: '#cafd00',
|
||
}}>{formatVolume(session.volume)}</span>
|
||
<span style={{
|
||
display: 'block',
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.65rem',
|
||
color: '#767575',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em',
|
||
}}>kg</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
{/* BOTTOM GLASSMORPHISM NAV */}
|
||
<nav
|
||
className="glass-nav"
|
||
style={{
|
||
position: 'fixed',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
padding: '0.625rem 0 0.75rem',
|
||
display: 'flex',
|
||
justifyContent: 'space-around',
|
||
alignItems: 'center',
|
||
borderTop: '1px solid rgba(255,255,255,0.06)',
|
||
zIndex: 100,
|
||
}}
|
||
>
|
||
{[
|
||
{ icon: 'home', label: 'Idag', nav: null, active: true },
|
||
{ icon: 'chart', label: 'Framsteg', nav: 'progress', active: false },
|
||
{ icon: 'target', label: 'Mål', nav: 'benchmarks', active: false },
|
||
{ icon: 'search', label: 'Övningar', nav: 'encyclopedia', active: false },
|
||
{ icon: 'user', label: 'Profil', nav: 'profile', active: false },
|
||
].map((item) => (
|
||
<button
|
||
key={item.label}
|
||
onClick={() => item.nav ? onNavigate(item.nav) : undefined}
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
gap: '0.25rem',
|
||
background: 'none',
|
||
border: 'none',
|
||
cursor: 'pointer',
|
||
padding: '0.25rem 0.75rem',
|
||
}}
|
||
>
|
||
<span style={{ color: item.active ? '#cafd00' : '#767575' }}>
|
||
<Icon name={item.icon} size={20} />
|
||
</span>
|
||
<span style={{
|
||
fontFamily: 'Space Grotesk, monospace',
|
||
fontSize: '0.6rem',
|
||
color: item.active ? '#cafd00' : '#767575',
|
||
letterSpacing: '0.04em',
|
||
textTransform: 'uppercase',
|
||
}}>{item.label}</span>
|
||
</button>
|
||
))}
|
||
</nav>
|
||
</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
|