stronglift-web/index.html
2026-02-10 12:33:07 +01:00

1564 lines
70 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StrongLifts 5x5 Tracker</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0a0e27;
--bg-secondary: #151a35;
--bg-card: #1a2140;
--accent-primary: #00e5ff;
--accent-secondary: #ff9100;
--text-primary: #ffffff;
--text-secondary: #8892b0;
--success: #00ff88;
--danger: #ff4757;
--border: #2a3555;
}
body {
font-family: 'Space Mono', monospace;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
padding: 30px 0;
border-bottom: 3px solid var(--accent-primary);
}
h1 {
font-family: 'Rajdhani', sans-serif;
font-size: 3.5rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.tagline {
font-size: 0.9rem;
color: var(--text-secondary);
letter-spacing: 3px;
text-transform: uppercase;
}
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid var(--border);
}
.nav-tab {
padding: 15px 25px;
background: transparent;
border: none;
color: var(--text-secondary);
font-family: 'Rajdhani', sans-serif;
font-size: 1.1rem;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s;
border-bottom: 3px solid transparent;
}
.nav-tab.active {
color: var(--accent-primary);
border-bottom-color: var(--accent-primary);
}
.nav-tab:hover {
color: var(--text-primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.workout-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.workout-card {
background: var(--bg-card);
border: 2px solid var(--border);
padding: 25px;
cursor: pointer;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.workout-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
transform: scaleX(0);
transition: transform 0.3s;
}
.workout-card:hover {
border-color: var(--accent-primary);
transform: translateY(-5px);
}
.workout-card:hover::before {
transform: scaleX(1);
}
.workout-card h3 {
font-family: 'Rajdhani', sans-serif;
font-size: 2rem;
margin-bottom: 15px;
color: var(--accent-primary);
}
.workout-card .exercises {
list-style: none;
}
.workout-card .exercises li {
padding: 8px 0;
color: var(--text-secondary);
font-size: 0.9rem;
}
.active-workout {
background: var(--bg-secondary);
padding: 30px;
border: 2px solid var(--accent-primary);
margin-bottom: 30px;
}
.exercise-block {
background: var(--bg-card);
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--accent-primary);
}
.exercise-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.exercise-name {
font-family: 'Rajdhani', sans-serif;
font-size: 1.8rem;
font-weight: 600;
color: var(--accent-primary);
}
.form-tip {
background: var(--bg-secondary);
padding: 10px 15px;
margin-bottom: 15px;
border-left: 3px solid var(--accent-secondary);
font-size: 0.85rem;
color: var(--text-secondary);
}
.sets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.set-card {
background: var(--bg-secondary);
padding: 15px;
text-align: center;
border: 2px solid var(--border);
transition: all 0.3s;
}
.set-card.completed {
border-color: var(--success);
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05));
}
.set-card.active {
border-color: var(--accent-primary);
box-shadow: 0 0 20px rgba(0, 229, 255, 0.3);
}
.set-number {
font-family: 'Rajdhani', sans-serif;
font-size: 1.2rem;
color: var(--text-secondary);
margin-bottom: 10px;
}
.weight-input {
width: 100%;
padding: 10px;
background: var(--bg-primary);
border: 2px solid var(--border);
color: var(--text-primary);
font-family: 'Space Mono', monospace;
font-size: 1.2rem;
text-align: center;
margin-bottom: 10px;
}
.weight-input:focus {
outline: none;
border-color: var(--accent-primary);
}
select.weight-input {
cursor: pointer;
text-align: left;
padding-left: 15px;
}
select.weight-input option {
background: var(--bg-primary);
color: var(--text-primary);
}
.complete-set-btn {
width: 100%;
padding: 10px;
background: var(--accent-primary);
color: var(--bg-primary);
border: none;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
}
.complete-set-btn:hover {
background: var(--text-primary);
transform: scale(1.05);
}
.complete-set-btn:disabled {
background: var(--border);
cursor: not-allowed;
transform: scale(1);
}
.rest-timer {
position: fixed;
bottom: 30px;
right: 30px;
background: var(--bg-card);
padding: 30px;
border: 3px solid var(--accent-primary);
min-width: 250px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
z-index: 1000;
animation: slideInRight 0.3s;
}
@keyframes slideInRight {
from { transform: translateX(400px); }
to { transform: translateX(0); }
}
.rest-timer.warning {
border-color: var(--accent-secondary);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.rest-timer h3 {
font-family: 'Rajdhani', sans-serif;
font-size: 1.2rem;
margin-bottom: 15px;
color: var(--text-secondary);
text-transform: uppercase;
}
.timer-display {
font-family: 'Rajdhani', sans-serif;
font-size: 3.5rem;
font-weight: 700;
color: var(--accent-primary);
margin-bottom: 15px;
}
.timer-controls {
display: flex;
gap: 10px;
}
.timer-btn {
flex: 1;
padding: 12px;
background: var(--bg-secondary);
border: 2px solid var(--border);
color: var(--text-primary);
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
}
.timer-btn:hover {
border-color: var(--accent-primary);
background: var(--bg-primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--bg-card);
padding: 25px;
border-left: 4px solid var(--accent-primary);
}
.stat-label {
font-size: 0.85rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.stat-value {
font-family: 'Rajdhani', sans-serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-primary);
}
.history-list {
background: var(--bg-card);
padding: 20px;
}
.history-item {
padding: 15px;
margin-bottom: 15px;
background: var(--bg-secondary);
border-left: 4px solid var(--accent-primary);
}
.history-date {
font-family: 'Rajdhani', sans-serif;
font-size: 1.2rem;
color: var(--accent-primary);
margin-bottom: 10px;
}
.history-exercises {
font-size: 0.85rem;
color: var(--text-secondary);
}
.btn-primary {
padding: 15px 30px;
background: var(--accent-primary);
color: var(--bg-primary);
border: none;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary:hover {
background: var(--text-primary);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 229, 255, 0.4);
}
.btn-secondary {
padding: 15px 30px;
background: transparent;
color: var(--text-primary);
border: 2px solid var(--border);
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-secondary:hover {
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.progress-chart {
background: var(--bg-card);
padding: 25px;
margin-bottom: 20px;
}
.chart-title {
font-family: 'Rajdhani', sans-serif;
font-size: 1.5rem;
margin-bottom: 20px;
color: var(--accent-primary);
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 10px;
height: 200px;
margin-bottom: 10px;
padding-bottom: 30px;
position: relative;
}
.chart-bar {
flex: 1;
background: linear-gradient(to top, var(--accent-primary), var(--accent-secondary));
min-height: 20px;
position: relative;
transition: all 0.3s;
}
.chart-bar:hover {
opacity: 0.8;
}
.chart-bar-label {
position: absolute;
bottom: -25px;
left: 0;
right: 0;
text-align: center;
font-size: 0.75rem;
color: var(--text-secondary);
}
.chart-bar-value {
position: absolute;
top: -20px;
left: 0;
right: 0;
text-align: center;
font-size: 0.8rem;
color: var(--text-primary);
font-weight: 700;
}
@media (max-width: 768px) {
h1 {
font-size: 2.5rem;
}
.rest-timer {
bottom: 20px;
right: 20px;
left: 20px;
min-width: auto;
}
.sets-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="app-container" id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
const EXERCISES = {
A: [
{ name: 'Squat', sets: 5, reps: 5, increment: 5, tip: 'Keep your chest up, break at hips and knees simultaneously. Bar should be over mid-foot.' },
{ name: 'Bench Press', sets: 5, reps: 5, increment: 2.5, tip: 'Retract shoulder blades, arch your back slightly. Lower bar to chest, press to lockout.' },
{ name: 'Barbell Row', sets: 5, reps: 5, increment: 2.5, tip: 'Hinge at hips, keep back flat. Pull bar to lower chest, squeeze shoulder blades.' }
],
B: [
{ name: 'Squat', sets: 5, reps: 5, increment: 5, tip: 'Keep your chest up, break at hips and knees simultaneously. Bar should be over mid-foot.' },
{ name: 'Overhead Press', sets: 5, reps: 5, increment: 2.5, tip: 'Start with bar on shoulders, press straight up. Lock out overhead, shrug at top.' },
{ name: 'Deadlift', sets: 1, reps: 5, increment: 5, tip: 'Bar over mid-foot, shins to bar. Pull slack out, push floor away with legs.' }
]
};
const DEFAULT_SETTINGS = {
restTimerLight: 90,
restTimerMedium: 180,
restTimerHeavy: 300,
soundEnabled: true,
unit: 'kg',
showWarmups: true,
cardioEnabled: true,
cardioFrequency: 2, // times per week
cardioType: 'LISS', // LISS, HIIT, or MISS
cardioDuration: 20 // minutes
};
// Calculate warm-up sets based on working weight
const calculateWarmups = (workWeight, exerciseName) => {
const warmups = [];
const barWeight = 20;
// Can't do empty bar for deadlift/row
const canStartWithBar = !['Deadlift', 'Barbell Row'].includes(exerciseName);
if (workWeight <= barWeight) {
return [{ weight: barWeight, reps: 5 }];
}
if (canStartWithBar && workWeight > 40) {
warmups.push({ weight: barWeight, reps: 5 });
warmups.push({ weight: barWeight, reps: 5 });
}
// Calculate progressive warm-up weights
const increments = [];
if (workWeight <= 60) {
increments.push(workWeight * 0.5);
} else if (workWeight <= 100) {
increments.push(workWeight * 0.4, workWeight * 0.6, workWeight * 0.8);
} else {
increments.push(workWeight * 0.4, workWeight * 0.6, workWeight * 0.75, workWeight * 0.9);
}
increments.forEach((weight, index) => {
const isLastWarmup = index === increments.length - 1;
warmups.push({
weight: Math.round(weight / 2.5) * 2.5,
reps: isLastWarmup ? 3 : 5
});
});
return warmups;
};
function App() {
const [activeTab, setActiveTab] = useState('workout');
const [selectedWorkout, setSelectedWorkout] = useState(null);
const [currentWorkout, setCurrentWorkout] = useState(null);
const [workoutHistory, setWorkoutHistory] = useState([]);
const [restTimer, setRestTimer] = useState(null);
const [timerActive, setTimerActive] = useState(false);
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const audioRef = useRef(null);
// Load data from storage
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const history = await window.storage.get('workout-history');
if (history) {
setWorkoutHistory(JSON.parse(history.value));
}
} catch (e) {
console.log('No previous history');
}
try {
const savedSettings = await window.storage.get('app-settings');
if (savedSettings) {
setSettings(JSON.parse(savedSettings.value));
}
} catch (e) {
console.log('No saved settings, using defaults');
}
};
const saveHistory = async (history) => {
try {
await window.storage.set('workout-history', JSON.stringify(history));
} catch (e) {
console.error('Failed to save history');
}
};
const saveSettings = async (newSettings) => {
try {
await window.storage.set('app-settings', JSON.stringify(newSettings));
setSettings(newSettings);
} catch (e) {
console.error('Failed to save settings');
}
};
const getNextWorkout = () => {
if (workoutHistory.length === 0) return 'A';
const lastWorkout = workoutHistory[0].workout;
return lastWorkout === 'A' ? 'B' : 'A';
};
const getLastWeight = (exerciseName) => {
for (let workout of workoutHistory) {
const exercise = workout.exercises.find(e => e.name === exerciseName);
if (exercise) {
return exercise.weight || 20; // Default starting weight
}
}
// Starting weights
if (exerciseName === 'Squat') return 20;
if (exerciseName === 'Deadlift') return 40;
return 20;
};
const getProgressionWeight = (exerciseName, lastWeight, increment) => {
const failures = getConsecutiveFailures(exerciseName);
if (failures >= 3) {
// Deload by 10%
return Math.round((lastWeight * 0.9) / 2.5) * 2.5;
}
return lastWeight + increment;
};
const getConsecutiveFailures = (exerciseName) => {
let failures = 0;
for (let workout of workoutHistory) {
const exercise = workout.exercises.find(e => e.name === exerciseName);
if (!exercise) break;
if (exercise.completed < exercise.sets) {
failures++;
} else {
break;
}
}
return failures;
};
const startWorkout = (workoutType) => {
const exercises = EXERCISES[workoutType].map(ex => ({
...ex,
weight: getProgressionWeight(ex.name, getLastWeight(ex.name), ex.increment),
sets: Array(ex.sets).fill().map(() => ({ completed: false, reps: ex.reps })),
currentSet: 0
}));
setCurrentWorkout({
workout: workoutType,
exercises,
startTime: new Date()
});
setSelectedWorkout(workoutType);
};
const completeSet = (exerciseIndex, setIndex) => {
const newWorkout = { ...currentWorkout };
newWorkout.exercises[exerciseIndex].sets[setIndex].completed = true;
// Move to next set if available
if (setIndex < newWorkout.exercises[exerciseIndex].sets.length - 1) {
newWorkout.exercises[exerciseIndex].currentSet = setIndex + 1;
startRestTimer(newWorkout.exercises[exerciseIndex].weight);
} else {
newWorkout.exercises[exerciseIndex].currentSet = -1; // All sets done
}
setCurrentWorkout(newWorkout);
};
const updateWeight = (exerciseIndex, weight) => {
const newWorkout = { ...currentWorkout };
newWorkout.exercises[exerciseIndex].weight = parseFloat(weight) || 0;
setCurrentWorkout(newWorkout);
};
const startRestTimer = (exerciseWeight) => {
// Determine rest time based on weight intensity
let restTime = settings.restTimerLight;
if (exerciseWeight > 80) {
restTime = settings.restTimerHeavy;
} else if (exerciseWeight > 50) {
restTime = settings.restTimerMedium;
}
setRestTimer(restTime);
setTimerActive(true);
};
const stopRestTimer = () => {
setRestTimer(null);
setTimerActive(false);
};
useEffect(() => {
let interval;
if (timerActive && restTimer > 0) {
interval = setInterval(() => {
setRestTimer(prev => {
if (prev <= 1) {
playNotification();
return 0;
}
return prev - 1;
});
}, 1000);
} else if (restTimer === 0) {
setTimerActive(false);
}
return () => clearInterval(interval);
}, [timerActive, restTimer]);
const playNotification = () => {
if (!settings.soundEnabled) return;
// Create beep sound
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
};
const finishWorkout = () => {
const workoutData = {
workout: currentWorkout.workout,
date: new Date().toISOString(),
exercises: currentWorkout.exercises.map(ex => ({
name: ex.name,
weight: ex.weight,
sets: ex.sets.length,
reps: ex.reps,
completed: ex.sets.filter(s => s.completed).length
})),
duration: Math.round((new Date() - new Date(currentWorkout.startTime)) / 60000)
};
const newHistory = [workoutData, ...workoutHistory];
setWorkoutHistory(newHistory);
saveHistory(newHistory);
setCurrentWorkout(null);
setSelectedWorkout(null);
stopRestTimer();
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const getWeeklyStats = () => {
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const weekWorkouts = workoutHistory.filter(w =>
new Date(w.date) > oneWeekAgo
);
const totalVolume = weekWorkouts.reduce((sum, workout) => {
return sum + workout.exercises.reduce((exSum, ex) => {
return exSum + (ex.weight * ex.completed * ex.reps);
}, 0);
}, 0);
return {
workouts: weekWorkouts.length,
volume: totalVolume,
avgDuration: weekWorkouts.length > 0
? Math.round(weekWorkouts.reduce((sum, w) => sum + w.duration, 0) / weekWorkouts.length)
: 0
};
};
const renderWorkoutSelector = () => (
<div>
{settings.cardioFrequency > 0 && (
<div style={{
background: 'var(--bg-secondary)',
padding: '20px',
marginBottom: '25px',
border: '2px solid var(--accent-secondary)',
textAlign: 'center'
}}>
<div style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', marginBottom: '5px' }}>
🏃 Cardio Reminder
</div>
<div style={{ color: 'var(--text-primary)', fontSize: '1.1rem' }}>
Do your cardio on <strong style={{ color: 'var(--accent-secondary)' }}>rest days</strong>, not before lifting!
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '5px' }}>
Pre-workout cardio will reduce your strength performance
</div>
</div>
)}
<div className="workout-selector">
<div className="workout-card" onClick={() => startWorkout('A')}>
<h3>WORKOUT A</h3>
<ul className="exercises">
{EXERCISES.A.map(ex => (
<li key={ex.name}>{ex.name} {ex.sets}x{ex.reps}</li>
))}
</ul>
</div>
<div className="workout-card" onClick={() => startWorkout('B')}>
<h3>WORKOUT B</h3>
<ul className="exercises">
{EXERCISES.B.map(ex => (
<li key={ex.name}>{ex.name} {ex.sets}x{ex.reps}</li>
))}
</ul>
</div>
</div>
</div>
);
const renderActiveWorkout = () => (
<div className="active-workout">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}>
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2.5rem', color: 'var(--accent-primary)' }}>
WORKOUT {currentWorkout.workout}
</h2>
<div style={{ display: 'flex', gap: '10px' }}>
<button className="btn-secondary" onClick={() => setCurrentWorkout(null)}>Cancel</button>
<button className="btn-primary" onClick={finishWorkout}>Finish Workout</button>
</div>
</div>
{currentWorkout.exercises.map((exercise, exIndex) => (
<div key={exIndex} className="exercise-block">
<div className="exercise-header">
<h3 className="exercise-name">{exercise.name}</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<label style={{ color: 'var(--text-secondary)' }}>Weight (kg):</label>
<input
type="number"
value={exercise.weight}
onChange={(e) => updateWeight(exIndex, e.target.value)}
className="weight-input"
style={{ width: '100px' }}
step="2.5"
/>
</div>
</div>
<div className="form-tip">
💡 {exercise.tip}
</div>
{/* Warm-up sets */}
{settings.showWarmups && (
<div style={{
background: 'var(--bg-secondary)',
padding: '15px',
marginBottom: '15px',
borderLeft: '3px solid var(--accent-secondary)'
}}>
<div style={{
fontSize: '0.9rem',
fontWeight: 'bold',
marginBottom: '10px',
color: 'var(--accent-secondary)',
textTransform: 'uppercase',
letterSpacing: '1px'
}}>
🔥 Ogrevanje (Warm-up)
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{calculateWarmups(exercise.weight, exercise.name).map((warmup, i) => (
<span key={i} style={{
background: 'var(--bg-card)',
padding: '5px 12px',
borderRadius: '3px',
border: '1px solid var(--border)'
}}>
{warmup.reps}×{warmup.weight}kg
</span>
))}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '8px', fontStyle: 'italic' }}>
No rest between warm-up sets. Rest 2-3 min before first working set.
</div>
</div>
)}
<div className="sets-grid">
{exercise.sets.map((set, setIndex) => (
<div
key={setIndex}
className={`set-card ${set.completed ? 'completed' : ''} ${exercise.currentSet === setIndex ? 'active' : ''}`}
>
<div className="set-number">SET {setIndex + 1}</div>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '10px', color: 'var(--accent-primary)' }}>
{set.reps} reps
</div>
<button
className="complete-set-btn"
onClick={() => completeSet(exIndex, setIndex)}
disabled={set.completed}
>
{set.completed ? '✓ DONE' : 'Complete'}
</button>
</div>
))}
</div>
</div>
))}
</div>
);
const renderHistory = () => {
const stats = getWeeklyStats();
// Get last workout date to recommend next cardio
const lastWorkoutDate = workoutHistory.length > 0
? new Date(workoutHistory[0].date)
: new Date();
const daysSinceLastWorkout = Math.floor((new Date() - lastWorkoutDate) / (1000 * 60 * 60 * 24));
return (
<div>
{settings.cardioFrequency > 0 && (
<div style={{
background: 'var(--bg-card)',
padding: '20px',
marginBottom: '30px',
borderLeft: '4px solid var(--accent-secondary)'
}}>
<h3 style={{
fontFamily: 'Rajdhani, sans-serif',
fontSize: '1.5rem',
marginBottom: '15px',
color: 'var(--accent-secondary)'
}}>
🏃 CARDIO SCHEDULE
</h3>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', lineHeight: '1.8' }}>
<strong style={{ color: 'var(--text-primary)' }}>Your Plan:</strong> {settings.cardioFrequency}× per week, {settings.cardioDuration} min of {settings.cardioType}<br/>
<strong style={{ color: 'var(--text-primary)' }}>Best Days:</strong> On rest days between strength workouts<br/>
<strong style={{ color: 'var(--text-primary)' }}>Example Schedule:</strong><br/>
<div style={{ marginTop: '10px', background: 'var(--bg-secondary)', padding: '15px' }}>
Monday: Workout A 🏋<br/>
Tuesday: <span style={{ color: 'var(--accent-secondary)', fontWeight: 'bold' }}>Cardio ({settings.cardioType})</span> 🏃<br/>
Wednesday: Workout B 🏋<br/>
Thursday: {settings.cardioFrequency >= 2 ? <span style={{ color: 'var(--accent-secondary)', fontWeight: 'bold' }}>Cardio ({settings.cardioType})</span> : 'Rest'} {settings.cardioFrequency >= 2 ? '🏃' : '😴'}<br/>
Friday: Workout A 🏋<br/>
Saturday: {settings.cardioFrequency >= 3 ? <span style={{ color: 'var(--accent-secondary)', fontWeight: 'bold' }}>Cardio (LISS/MISS)</span> : 'Rest'} {settings.cardioFrequency >= 3 ? '🏃' : '😴'}<br/>
Sunday: Rest 😴
</div>
<div style={{
marginTop: '15px',
padding: '10px',
background: 'var(--bg-primary)',
fontSize: '0.85rem',
borderLeft: '3px solid var(--success)'
}}>
💡 <strong>Pro Tip:</strong> Mix cardio types during the week. LISS is best for recovery days, HIIT when you have more energy.
</div>
</div>
</div>
)}
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2rem', marginBottom: '20px', color: 'var(--accent-primary)' }}>
WEEKLY SUMMARY
</h2>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Workouts This Week</div>
<div className="stat-value">{stats.workouts}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Volume (kg)</div>
<div className="stat-value">{stats.volume}</div>
</div>
<div className="stat-card">
<div className="stat-label">Avg Duration (min)</div>
<div className="stat-value">{stats.avgDuration}</div>
</div>
{settings.cardioFrequency > 0 && (
<div className="stat-card" style={{ borderLeft: '4px solid var(--accent-secondary)' }}>
<div className="stat-label">Cardio Goal/Week</div>
<div className="stat-value" style={{ color: 'var(--accent-secondary)', fontSize: '2rem' }}>
{settings.cardioFrequency}×
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '5px' }}>
{settings.cardioDuration} min {settings.cardioType}
</div>
</div>
)}
</div>
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2rem', marginBottom: '20px', marginTop: '40px', color: 'var(--accent-primary)' }}>
WORKOUT HISTORY
</h2>
<div className="history-list">
{workoutHistory.length === 0 ? (
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', padding: '40px' }}>
No workouts recorded yet. Start your first workout!
</p>
) : (
workoutHistory.map((workout, index) => (
<div key={index} className="history-item">
<div className="history-date">
{new Date(workout.date).toLocaleDateString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
})} - WORKOUT {workout.workout} ({workout.duration} min)
</div>
<div className="history-exercises">
{workout.exercises.map((ex, i) => (
<div key={i} style={{ marginBottom: '5px' }}>
{ex.name}: {ex.completed}/{ex.sets} sets × {ex.reps} reps @ {ex.weight}kg
</div>
))}
</div>
</div>
))
)}
</div>
</div>
);
};
const renderProgress = () => {
// Get last 10 workouts for each exercise
const exerciseData = {};
const personalRecords = {};
let totalVolumeByWeek = {};
let successRateData = {};
workoutHistory.forEach((workout, index) => {
// Process each exercise
workout.exercises.forEach(ex => {
// Exercise weight progression
if (!exerciseData[ex.name]) {
exerciseData[ex.name] = [];
personalRecords[ex.name] = 0;
successRateData[ex.name] = { completed: 0, total: 0 };
}
if (exerciseData[ex.name].length < 10) {
exerciseData[ex.name].push({
date: new Date(workout.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
weight: ex.weight,
fullDate: new Date(workout.date)
});
}
// Track personal records
if (ex.weight > personalRecords[ex.name]) {
personalRecords[ex.name] = ex.weight;
}
// Track success rate
successRateData[ex.name].total += ex.sets;
successRateData[ex.name].completed += ex.completed;
// Calculate weekly volume
const weekKey = getWeekKey(new Date(workout.date));
if (!totalVolumeByWeek[weekKey]) {
totalVolumeByWeek[weekKey] = 0;
}
totalVolumeByWeek[weekKey] += ex.weight * ex.completed * ex.reps;
});
});
// Reverse data for chronological order
Object.keys(exerciseData).forEach(key => {
exerciseData[key].reverse();
});
// Prepare weekly volume data
const weeklyVolumeData = Object.entries(totalVolumeByWeek)
.sort((a, b) => new Date(a[0]) - new Date(b[0]))
.slice(-8);
const maxVolume = Math.max(...weeklyVolumeData.map(([_, vol]) => vol), 1);
return (
<div>
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2rem', marginBottom: '20px', color: 'var(--accent-primary)' }}>
PROGRESS ANALYTICS
</h2>
{/* Personal Records */}
<h3 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '1.5rem', marginBottom: '15px', marginTop: '30px', color: 'var(--text-primary)' }}>
Personal Records
</h3>
<div className="stats-grid" style={{ marginBottom: '40px' }}>
{Object.entries(personalRecords).map(([exercise, weight]) => (
<div key={exercise} className="stat-card">
<div className="stat-label">{exercise}</div>
<div className="stat-value">{weight} kg</div>
</div>
))}
</div>
{/* Success Rate */}
<h3 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '1.5rem', marginBottom: '15px', marginTop: '30px', color: 'var(--text-primary)' }}>
Success Rate
</h3>
<div className="stats-grid" style={{ marginBottom: '40px' }}>
{Object.entries(successRateData).map(([exercise, data]) => {
const percentage = data.total > 0 ? Math.round((data.completed / data.total) * 100) : 0;
return (
<div key={exercise} className="stat-card">
<div className="stat-label">{exercise}</div>
<div className="stat-value" style={{
color: percentage >= 90 ? 'var(--success)' : percentage >= 70 ? 'var(--accent-secondary)' : 'var(--danger)'
}}>
{percentage}%
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '5px' }}>
{data.completed}/{data.total} sets completed
</div>
</div>
);
})}
</div>
{/* Weekly Volume Chart */}
{weeklyVolumeData.length > 0 && (
<div className="progress-chart" style={{ marginBottom: '40px' }}>
<h3 className="chart-title">Weekly Total Volume (Last 8 Weeks)</h3>
<div className="chart-bars">
{weeklyVolumeData.map(([week, volume], index) => (
<div
key={index}
className="chart-bar"
style={{ height: `${(volume / maxVolume) * 100}%` }}
>
<span className="chart-bar-value">{Math.round(volume / 1000)}k</span>
<span className="chart-bar-label">{formatWeekLabel(week)}</span>
</div>
))}
</div>
<div style={{ textAlign: 'center', marginTop: '30px', color: 'var(--text-secondary)' }}>
Total volume = Weight × Reps × Sets
</div>
</div>
)}
{/* Exercise Weight Progression */}
<h3 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '1.5rem', marginBottom: '15px', marginTop: '30px', color: 'var(--text-primary)' }}>
Weight Progression (Last 10 Sessions)
</h3>
{Object.keys(exerciseData).map(exerciseName => {
const data = exerciseData[exerciseName];
const maxWeight = Math.max(...data.map(d => d.weight));
const minWeight = Math.min(...data.map(d => d.weight));
const improvement = data.length > 1 ? data[data.length - 1].weight - data[0].weight : 0;
return (
<div key={exerciseName} className="progress-chart">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<h3 className="chart-title" style={{ marginBottom: 0 }}>{exerciseName}</h3>
<div style={{
color: improvement > 0 ? 'var(--success)' : improvement < 0 ? 'var(--danger)' : 'var(--text-secondary)',
fontSize: '1rem',
fontWeight: 'bold'
}}>
{improvement > 0 ? '+' : ''}{improvement} kg
</div>
</div>
<div className="chart-bars">
{data.map((point, index) => (
<div
key={index}
className="chart-bar"
style={{ height: `${((point.weight - minWeight) / (maxWeight - minWeight || 1)) * 100 + 20}%` }}
>
<span className="chart-bar-value">{point.weight}</span>
<span className="chart-bar-label">{point.date}</span>
</div>
))}
</div>
</div>
);
})}
{Object.keys(exerciseData).length === 0 && (
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', padding: '40px' }}>
Complete some workouts to see your progress!
</p>
)}
</div>
);
};
const getWeekKey = (date) => {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust to Monday
const monday = new Date(d.setDate(diff));
return monday.toISOString().split('T')[0];
};
const formatWeekLabel = (weekKey) => {
const date = new Date(weekKey);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
const renderSettings = () => (
<div>
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2rem', marginBottom: '20px', color: 'var(--accent-primary)' }}>
SETTINGS
</h2>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Light Weight Rest (sec)</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
For sets 50kg
</div>
<input
type="number"
value={settings.restTimerLight}
onChange={(e) => saveSettings({ ...settings, restTimerLight: parseInt(e.target.value) || 90 })}
className="weight-input"
style={{ fontSize: '1.5rem' }}
min="30"
max="600"
step="15"
/>
</div>
<div className="stat-card">
<div className="stat-label">Medium Weight Rest (sec)</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
For sets 50-80kg
</div>
<input
type="number"
value={settings.restTimerMedium}
onChange={(e) => saveSettings({ ...settings, restTimerMedium: parseInt(e.target.value) || 180 })}
className="weight-input"
style={{ fontSize: '1.5rem' }}
min="30"
max="600"
step="15"
/>
</div>
<div className="stat-card">
<div className="stat-label">Heavy Weight Rest (sec)</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
For sets >80kg
</div>
<input
type="number"
value={settings.restTimerHeavy}
onChange={(e) => saveSettings({ ...settings, restTimerHeavy: parseInt(e.target.value) || 300 })}
className="weight-input"
style={{ fontSize: '1.5rem' }}
min="30"
max="600"
step="15"
/>
</div>
</div>
<div className="stat-card" style={{ marginTop: '30px' }}>
<div className="stat-label">Notifications</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', marginTop: '15px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontSize: '1.1rem' }}>
<input
type="checkbox"
checked={settings.soundEnabled}
onChange={(e) => saveSettings({ ...settings, soundEnabled: e.target.checked })}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
<span>Enable sound notification when rest timer ends</span>
</label>
</div>
</div>
<div className="stat-card" style={{ marginTop: '30px' }}>
<div className="stat-label">Warm-up Sets (Ogrevanje)</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', marginTop: '15px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontSize: '1.1rem' }}>
<input
type="checkbox"
checked={settings.showWarmups}
onChange={(e) => saveSettings({ ...settings, showWarmups: e.target.checked })}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
<span>Show recommended warm-up sets before working sets</span>
</label>
</div>
</div>
<h2 style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: '2rem', marginTop: '40px', marginBottom: '20px', color: 'var(--accent-primary)' }}>
CARDIO SETTINGS
</h2>
<div className="stat-card" style={{ background: 'var(--bg-secondary)', borderLeft: '4px solid var(--accent-secondary)' }}>
<div className="stat-label" style={{ color: 'var(--accent-secondary)' }}>💡 Cardio Recommendations</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '10px', lineHeight: '1.8' }}>
<strong style={{ color: 'var(--text-primary)' }}>When to do cardio:</strong> Always on rest days (NOT on strength training days)<br/>
<strong style={{ color: 'var(--text-primary)' }}>Frequency:</strong> 2-3 sessions per week, 10-25 minutes each<br/>
<strong style={{ color: 'var(--text-primary)' }}>Best types:</strong> Low-impact (cycling, swimming) over high-impact (running)<br/>
<strong style={{ color: 'var(--text-primary)' }}>Never:</strong> Do intense cardio before lifting - it will reduce your strength performance
</div>
</div>
<div className="stats-grid" style={{ marginTop: '20px' }}>
<div className="stat-card">
<div className="stat-label">Weekly Cardio Sessions</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
On rest days only
</div>
<input
type="number"
value={settings.cardioFrequency}
onChange={(e) => saveSettings({ ...settings, cardioFrequency: parseInt(e.target.value) || 0 })}
className="weight-input"
style={{ fontSize: '1.5rem' }}
min="0"
max="4"
/>
</div>
<div className="stat-card">
<div className="stat-label">Default Duration (min)</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
10-25 min recommended
</div>
<input
type="number"
value={settings.cardioDuration}
onChange={(e) => saveSettings({ ...settings, cardioDuration: parseInt(e.target.value) || 20 })}
className="weight-input"
style={{ fontSize: '1.5rem' }}
min="5"
max="60"
step="5"
/>
</div>
<div className="stat-card">
<div className="stat-label">Preferred Type</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.8rem', marginBottom: '10px' }}>
Cardio intensity
</div>
<select
value={settings.cardioType}
onChange={(e) => saveSettings({ ...settings, cardioType: e.target.value })}
className="weight-input"
style={{ fontSize: '1.1rem' }}
>
<option value="LISS">LISS - Low Intensity (60-70% HR)</option>
<option value="MISS">MISS - Medium Intensity (70-80% HR)</option>
<option value="HIIT">HIIT - High Intensity Intervals</option>
</select>
</div>
</div>
<div className="stat-card" style={{ marginTop: '20px' }}>
<div className="stat-label">Cardio Type Guide</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginTop: '10px', lineHeight: '1.6' }}>
<div style={{ marginBottom: '10px', paddingBottom: '10px', borderBottom: '1px solid var(--border)' }}>
<strong style={{ color: 'var(--accent-primary)' }}>LISS (Low Intensity Steady State)</strong><br/>
Easy pace you can maintain for 20-40 min. You should be able to have a conversation.<br/>
Examples: Easy cycling, walking on incline, light swimming
</div>
<div style={{ marginBottom: '10px', paddingBottom: '10px', borderBottom: '1px solid var(--border)' }}>
<strong style={{ color: 'var(--accent-primary)' }}>MISS (Medium Intensity Steady State)</strong><br/>
Moderate pace, somewhat challenging but sustainable. Conversation becomes harder.<br/>
Examples: Moderate cycling, jogging, rowing at steady pace
</div>
<div>
<strong style={{ color: 'var(--accent-primary)' }}>HIIT (High Intensity Interval Training)</strong><br/>
Short bursts of maximum effort followed by rest. Very challenging, can't talk during work intervals.<br/>
Examples: Sprint intervals (30sec on / 60sec off), bike sprints, battle ropes
</div>
</div>
<div style={{
marginTop: '15px',
padding: '12px',
background: 'var(--bg-primary)',
borderLeft: '3px solid var(--accent-secondary)',
fontSize: '0.85rem',
color: 'var(--text-secondary)'
}}>
<strong style={{ color: 'var(--accent-secondary)' }}>Recommendation:</strong> Do 2-3 different types per week for best results. Example: LISS on Tuesday, HIIT on Thursday, LISS on Saturday
</div>
</div>
<div className="stat-card" style={{ marginTop: '30px', background: 'var(--bg-secondary)' }}>
<div className="stat-label">Data Storage</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '10px', lineHeight: '1.6' }}>
All your workout data is stored persistently in your browser. Your workout history, settings,
and progress are automatically saved and will be available when you return to this app.
</div>
<div style={{ marginTop: '15px', padding: '10px', background: 'var(--bg-card)', borderLeft: '3px solid var(--accent-primary)' }}>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
💾 Total workouts logged: <span style={{ color: 'var(--accent-primary)', fontWeight: 'bold' }}>{workoutHistory.length}</span>
</div>
</div>
</div>
<div className="stat-card" style={{ marginTop: '30px', borderLeft: '4px solid var(--danger)' }}>
<div className="stat-label" style={{ color: 'var(--danger)' }}>Danger Zone</div>
<button
className="btn-secondary"
style={{ marginTop: '15px', borderColor: 'var(--danger)', color: 'var(--danger)' }}
onClick={() => {
if (confirm('Are you sure you want to clear all workout history? This cannot be undone!')) {
setWorkoutHistory([]);
saveHistory([]);
}
}}
>
Clear All Workout History
</button>
</div>
</div>
);
return (
<div>
<header>
<h1>STRONGLIFTS 5×5</h1>
<div className="tagline">Get Strong • Build Muscle • Dominate</div>
</header>
<div className="nav-tabs">
<button
className={`nav-tab ${activeTab === 'workout' ? 'active' : ''}`}
onClick={() => setActiveTab('workout')}
>
Workout
</button>
<button
className={`nav-tab ${activeTab === 'history' ? 'active' : ''}`}
onClick={() => setActiveTab('history')}
>
History
</button>
<button
className={`nav-tab ${activeTab === 'progress' ? 'active' : ''}`}
onClick={() => setActiveTab('progress')}
>
Progress
</button>
<button
className={`nav-tab ${activeTab === 'settings' ? 'active' : ''}`}
onClick={() => setActiveTab('settings')}
>
Settings
</button>
</div>
<div className={`tab-content ${activeTab === 'workout' ? 'active' : ''}`}>
{currentWorkout ? renderActiveWorkout() : renderWorkoutSelector()}
{!currentWorkout && (
<div style={{ textAlign: 'center', marginTop: '30px' }}>
<p style={{ color: 'var(--text-secondary)', marginBottom: '20px' }}>
Next recommended workout: <span style={{ color: 'var(--accent-primary)', fontWeight: 'bold' }}>WORKOUT {getNextWorkout()}</span>
</p>
</div>
)}
</div>
<div className={`tab-content ${activeTab === 'history' ? 'active' : ''}`}>
{renderHistory()}
</div>
<div className={`tab-content ${activeTab === 'progress' ? 'active' : ''}`}>
{renderProgress()}
</div>
<div className={`tab-content ${activeTab === 'settings' ? 'active' : ''}`}>
{renderSettings()}
</div>
{restTimer !== null && (
<div className={`rest-timer ${restTimer <= 10 ? 'warning' : ''}`}>
<h3>Rest Period</h3>
<div className="timer-display">{formatTime(restTimer)}</div>
<div className="timer-controls">
<button className="timer-btn" onClick={stopRestTimer}>Skip</button>
<button className="timer-btn" onClick={() => {
// Reset to the original timer value based on current weight
if (currentWorkout) {
const currentEx = currentWorkout.exercises.find(ex => ex.currentSet >= 0);
if (currentEx) {
startRestTimer(currentEx.weight);
}
}
}}>Reset</button>
</div>
</div>
)}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
</script>
</body>
</html>