1564 lines
70 KiB
HTML
1564 lines
70 KiB
HTML
|
|
<!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>
|