feat(frontend): Kinetic Precision design system — new lime theme, glassmorphism, redesigned pages
- 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)
This commit is contained in:
@@ -2,6 +2,7 @@ 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'
|
||||
|
||||
@@ -11,7 +12,6 @@ const getCoachGreeting = (user, todayWorkout) => {
|
||||
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) {
|
||||
@@ -22,7 +22,6 @@ const getCoachGreeting = (user, todayWorkout) => {
|
||||
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) {
|
||||
@@ -35,7 +34,6 @@ const getCoachGreeting = (user, todayWorkout) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Rest day tips
|
||||
const restDayTips = [
|
||||
{ iconName: 'walking', text: 'Promenad' },
|
||||
{ iconName: 'yoga', text: 'Stretching' },
|
||||
@@ -43,15 +41,42 @@ const restDayTips = [
|
||||
{ iconName: 'cycling', text: 'Cykling' },
|
||||
]
|
||||
|
||||
// Get weekday names
|
||||
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()
|
||||
@@ -62,25 +87,42 @@ function Dashboard({ onStartWorkout, onNavigate }) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 className="dashboard loading">
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0e0e0e' }}>
|
||||
<div className="spinner"></div>
|
||||
<p>Laddar...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -88,131 +130,501 @@ function Dashboard({ onStartWorkout, onNavigate }) {
|
||||
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('encyclopedia')} title="Exercise Encyclopedia"><Icon name="search" 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>
|
||||
<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 className="dashboard-main">
|
||||
{/* Week Calendar - TOP */}
|
||||
<section className="week-calendar">
|
||||
<div className="calendar-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
|
||||
className="calendar-nav"
|
||||
onClick={() => setCurrentWeekStart(addDays(currentWeekStart, -7))}
|
||||
style={{ background: 'none', border: 'none', color: '#adaaaa', cursor: 'pointer', padding: '0.25rem' }}
|
||||
>
|
||||
<Icon name="chevronLeft" size={16} />
|
||||
</button>
|
||||
<span className="calendar-title">
|
||||
<span style={{
|
||||
fontFamily: 'Space Grotesk, monospace',
|
||||
fontSize: '0.75rem',
|
||||
color: '#adaaaa',
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
{formatWeekRange(currentWeekStart)}
|
||||
</span>
|
||||
<button
|
||||
className="calendar-nav"
|
||||
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 className="calendar-days">
|
||||
<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}
|
||||
className={`calendar-day ${isToday ? 'today' : ''} ${hasWorkout ? 'has-workout' : ''}`}
|
||||
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 className="day-name">{name}</span>
|
||||
<span className="day-date">{date.getDate()}</span>
|
||||
{hasWorkout && <span className="day-dot" />}
|
||||
<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 Section with Today's Action */}
|
||||
<section className="coach-section">
|
||||
<div className="coach-greeting">
|
||||
<div className="coach-avatar">
|
||||
<Icon name="coach" size={36} />
|
||||
{/* 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 className="coach-message">
|
||||
<p>{getCoachGreeting(user, todayWorkout)}</p>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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 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 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>
|
||||
)
|
||||
}
|
||||
@@ -232,16 +644,16 @@ function addDays(date, days) {
|
||||
}
|
||||
|
||||
function isSameDay(d1, d2) {
|
||||
return d1.getDate() === d2.getDate() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getFullYear() === d2.getFullYear()
|
||||
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}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user