stronglift-web/index.html

1564 lines
70 KiB
HTML
Raw Normal View History

2026-02-10 11:33:07 +00:00
<!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>