diff --git a/frontend/css/styles.css b/frontend/css/styles.css
new file mode 100644
index 0000000..c120c56
--- /dev/null
+++ b/frontend/css/styles.css
@@ -0,0 +1,476 @@
+/* Reset and Base Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary-color: #4f46e5;
+ --primary-dark: #4338ca;
+ --secondary-color: #6b7280;
+ --success-color: #10b981;
+ --danger-color: #ef4444;
+ --background-color: #f3f4f6;
+ --card-background: #ffffff;
+ --text-color: #1f2937;
+ --text-secondary: #6b7280;
+ --border-color: #e5e7eb;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
+ --radius: 12px;
+ --radius-sm: 8px;
+ --nav-height: 64px;
+ --header-height: 56px;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ line-height: 1.5;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* Header */
+.header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: var(--header-height);
+ background: var(--card-background);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+ padding: 0 16px;
+}
+
+.header-title {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+/* Main Content */
+.main-content {
+ flex: 1;
+ padding: calc(var(--header-height) + 16px) 16px calc(var(--nav-height) + 16px);
+ max-width: 600px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* Views */
+.view {
+ display: none;
+}
+
+.view.active {
+ display: block;
+}
+
+.view-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+}
+
+.view-header h2 {
+ font-size: 24px;
+ font-weight: 600;
+}
+
+/* Sections */
+.section {
+ margin-top: 24px;
+}
+
+.section h3 {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 12px;
+}
+
+.section h4 {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 20px;
+ border: none;
+ border-radius: var(--radius-sm);
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-primary {
+ background: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--primary-dark);
+}
+
+.btn-secondary {
+ background: var(--border-color);
+ color: var(--text-color);
+}
+
+.btn-secondary:hover {
+ background: #d1d5db;
+}
+
+.btn-text {
+ background: transparent;
+ color: var(--primary-color);
+ padding: 8px 12px;
+}
+
+.btn-danger {
+ background: var(--danger-color);
+ color: white;
+}
+
+.btn-large {
+ width: 100%;
+ padding: 14px 24px;
+ font-size: 16px;
+}
+
+.btn-small {
+ padding: 6px 12px;
+ font-size: 12px;
+}
+
+/* Quick Actions */
+.quick-actions {
+ margin-bottom: 24px;
+}
+
+/* Cards */
+.card-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.card {
+ background: var(--card-background);
+ border-radius: var(--radius);
+ padding: 16px;
+ box-shadow: var(--shadow);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 8px;
+}
+
+.card-title {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.card-subtitle {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.card-body {
+ margin-top: 8px;
+}
+
+.card-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.badge-strength {
+ background: #dbeafe;
+ color: #1e40af;
+}
+
+.badge-cardio {
+ background: #fef3c7;
+ color: #92400e;
+}
+
+/* Empty State */
+.empty-state {
+ text-align: center;
+ color: var(--text-secondary);
+ padding: 32px 16px;
+}
+
+/* Forms */
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.form-group label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ font-size: 16px;
+ background: var(--card-background);
+ color: var(--text-color);
+}
+
+.form-group input:focus,
+.form-group select:focus,
+.form-group textarea:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 16px;
+ overflow-x: auto;
+ padding-bottom: 4px;
+}
+
+.tab {
+ padding: 8px 16px;
+ border: none;
+ background: var(--border-color);
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: all 0.2s;
+}
+
+.tab.active {
+ background: var(--primary-color);
+ color: white;
+}
+
+/* Modal */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ z-index: 200;
+}
+
+.modal.hidden {
+ display: none;
+}
+
+.modal-content {
+ background: var(--card-background);
+ border-radius: var(--radius) var(--radius) 0 0;
+ padding: 20px;
+ width: 100%;
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.modal-header h3 {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+/* Bottom Navigation */
+.bottom-nav {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: var(--nav-height);
+ background: var(--card-background);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 16px;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: color 0.2s;
+}
+
+.nav-item.active {
+ color: var(--primary-color);
+}
+
+.nav-icon {
+ font-size: 20px;
+}
+
+.nav-label {
+ font-size: 11px;
+ font-weight: 500;
+}
+
+/* Plan Exercise Item */
+.plan-exercise-item {
+ display: flex;
+ gap: 8px;
+ align-items: flex-start;
+ padding: 12px;
+ background: var(--background-color);
+ border-radius: var(--radius-sm);
+ margin-bottom: 8px;
+}
+
+.plan-exercise-item select {
+ flex: 1;
+}
+
+.plan-exercise-item input {
+ width: 70px;
+}
+
+.plan-exercise-item .btn {
+ padding: 8px;
+}
+
+/* Session Entry */
+.session-entry {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ background: var(--background-color);
+ border-radius: var(--radius-sm);
+}
+
+.entry-info {
+ flex: 1;
+}
+
+.entry-name {
+ font-weight: 500;
+}
+
+.entry-details {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+/* Utilities */
+.hidden {
+ display: none !important;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.mt-2 {
+ margin-top: 8px;
+}
+
+.mt-4 {
+ margin-top: 16px;
+}
+
+/* Responsive adjustments for larger screens */
+@media (min-width: 640px) {
+ .modal-content {
+ border-radius: var(--radius);
+ margin-bottom: 20px;
+ }
+
+ .modal {
+ align-items: center;
+ }
+}
+
+@media (min-width: 768px) {
+ :root {
+ --header-height: 64px;
+ }
+
+ .header-title {
+ font-size: 20px;
+ }
+
+ .main-content {
+ padding: calc(var(--header-height) + 24px) 24px calc(var(--nav-height) + 24px);
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..c43edb9
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,259 @@
+
+
+
+
+
+ Training Tracker
+
+
+
+
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Exercises
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/js/api.js b/frontend/js/api.js
new file mode 100644
index 0000000..a067d79
--- /dev/null
+++ b/frontend/js/api.js
@@ -0,0 +1,139 @@
+// API Client for Training Tracker
+
+const API_BASE_URL = 'http://localhost:8080/api';
+
+class ApiClient {
+ constructor(baseUrl = API_BASE_URL) {
+ this.baseUrl = baseUrl;
+ }
+
+ async request(endpoint, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+ const config = {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ ...options,
+ };
+
+ if (config.body && typeof config.body === 'object') {
+ config.body = JSON.stringify(config.body);
+ }
+
+ try {
+ const response = await fetch(url, config);
+
+ if (response.status === 204) {
+ return null;
+ }
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'API request failed');
+ }
+
+ return data;
+ } catch (error) {
+ console.error('API Error:', error);
+ throw error;
+ }
+ }
+
+ // Exercises
+ async getExercises() {
+ return this.request('/exercises');
+ }
+
+ async getExercise(id) {
+ return this.request(`/exercises/${id}`);
+ }
+
+ async createExercise(data) {
+ return this.request('/exercises', {
+ method: 'POST',
+ body: data,
+ });
+ }
+
+ async updateExercise(id, data) {
+ return this.request(`/exercises/${id}`, {
+ method: 'PUT',
+ body: data,
+ });
+ }
+
+ async deleteExercise(id) {
+ return this.request(`/exercises/${id}`, {
+ method: 'DELETE',
+ });
+ }
+
+ // Training Plans
+ async getPlans() {
+ return this.request('/plans');
+ }
+
+ async getPlan(id) {
+ return this.request(`/plans/${id}`);
+ }
+
+ async createPlan(data) {
+ return this.request('/plans', {
+ method: 'POST',
+ body: data,
+ });
+ }
+
+ async updatePlan(id, data) {
+ return this.request(`/plans/${id}`, {
+ method: 'PUT',
+ body: data,
+ });
+ }
+
+ async deletePlan(id) {
+ return this.request(`/plans/${id}`, {
+ method: 'DELETE',
+ });
+ }
+
+ // Sessions
+ async getSessions() {
+ return this.request('/sessions');
+ }
+
+ async getSession(id) {
+ return this.request(`/sessions/${id}`);
+ }
+
+ async createSession(data) {
+ return this.request('/sessions', {
+ method: 'POST',
+ body: data,
+ });
+ }
+
+ async updateSession(id, data) {
+ return this.request(`/sessions/${id}`, {
+ method: 'PUT',
+ body: data,
+ });
+ }
+
+ async deleteSession(id) {
+ return this.request(`/sessions/${id}`, {
+ method: 'DELETE',
+ });
+ }
+
+ async addSessionEntry(sessionId, data) {
+ return this.request(`/sessions/${sessionId}/entries`, {
+ method: 'POST',
+ body: data,
+ });
+ }
+}
+
+const api = new ApiClient();
diff --git a/frontend/js/app.js b/frontend/js/app.js
new file mode 100644
index 0000000..0931aa6
--- /dev/null
+++ b/frontend/js/app.js
@@ -0,0 +1,718 @@
+// Training Tracker App
+
+const app = {
+ currentView: 'dashboard',
+ exercises: [],
+ plans: [],
+ sessions: [],
+ currentSession: null,
+ currentPlan: null,
+ exerciseFilter: 'all',
+
+ // Initialize the app
+ async init() {
+ this.setupNavigation();
+ this.setupForms();
+ this.setupTabs();
+ await this.loadData();
+ this.render();
+ },
+
+ // Setup navigation
+ setupNavigation() {
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const view = item.dataset.view;
+ this.navigateTo(view);
+ });
+ });
+ },
+
+ // Navigate to a view
+ navigateTo(viewName) {
+ // Hide all views
+ document.querySelectorAll('.view').forEach(view => {
+ view.classList.remove('active');
+ });
+
+ // Show target view
+ const targetView = document.getElementById(`view-${viewName}`);
+ if (targetView) {
+ targetView.classList.add('active');
+ }
+
+ // Update nav items
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.classList.toggle('active', item.dataset.view === viewName);
+ });
+
+ this.currentView = viewName;
+ this.render();
+ },
+
+ // Setup form handlers
+ setupForms() {
+ // Exercise form
+ document.getElementById('exercise-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ await this.saveExercise();
+ });
+
+ // Plan form
+ document.getElementById('plan-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ await this.savePlan();
+ });
+
+ // Session form
+ document.getElementById('session-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ await this.saveSession();
+ });
+
+ // Entry form
+ document.getElementById('entry-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ await this.saveEntry();
+ });
+
+ // Exercise select change (for entry form)
+ document.getElementById('entry-exercise').addEventListener('change', (e) => {
+ this.updateEntryFields(e.target.value);
+ });
+ },
+
+ // Setup tabs
+ setupTabs() {
+ document.querySelectorAll('.tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ const filter = tab.dataset.filter;
+ this.exerciseFilter = filter;
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ this.renderExercises();
+ });
+ });
+ },
+
+ // Load all data
+ async loadData() {
+ try {
+ [this.exercises, this.plans, this.sessions] = await Promise.all([
+ api.getExercises(),
+ api.getPlans(),
+ api.getSessions(),
+ ]);
+ } catch (error) {
+ console.error('Failed to load data:', error);
+ }
+ },
+
+ // Render current view
+ render() {
+ switch (this.currentView) {
+ case 'dashboard':
+ this.renderDashboard();
+ break;
+ case 'sessions':
+ this.renderSessions();
+ break;
+ case 'exercises':
+ this.renderExercises();
+ break;
+ case 'plans':
+ this.renderPlans();
+ break;
+ }
+ },
+
+ // Render dashboard
+ renderDashboard() {
+ const container = document.getElementById('recent-sessions');
+ const recentSessions = this.sessions.slice(0, 5);
+
+ if (recentSessions.length === 0) {
+ container.innerHTML = 'No sessions yet. Start your first workout!
';
+ return;
+ }
+
+ container.innerHTML = recentSessions.map(session => this.renderSessionCard(session)).join('');
+ },
+
+ // Render sessions list
+ renderSessions() {
+ const container = document.getElementById('sessions-list');
+
+ if (this.sessions.length === 0) {
+ container.innerHTML = 'No sessions yet
';
+ return;
+ }
+
+ container.innerHTML = this.sessions.map(session => this.renderSessionCard(session)).join('');
+ },
+
+ // Render session card
+ renderSessionCard(session) {
+ const date = new Date(session.date).toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ });
+ const planName = session.plan ? session.plan.name : 'No plan';
+ const entryCount = session.entries ? session.entries.length : 0;
+
+ return `
+
+
+ ${session.notes ? `
${session.notes}
` : ''}
+
+ `;
+ },
+
+ // View session details
+ async viewSession(id) {
+ try {
+ this.currentSession = await api.getSession(id);
+ this.renderSessionDetail();
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
+ document.getElementById('view-session-detail').classList.add('active');
+ } catch (error) {
+ alert('Failed to load session');
+ }
+ },
+
+ // Render session detail
+ renderSessionDetail() {
+ const session = this.currentSession;
+ if (!session) return;
+
+ const date = new Date(session.date).toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+
+ document.getElementById('session-detail-title').textContent = date;
+
+ let html = '';
+ if (session.plan) {
+ html += `Plan: ${session.plan.name}
`;
+ }
+ if (session.notes) {
+ html += `${session.notes}
`;
+ }
+
+ html += 'Exercises
';
+
+ if (session.entries && session.entries.length > 0) {
+ session.entries.forEach(entry => {
+ const exercise = entry.exercise || {};
+ let details = '';
+
+ if (exercise.type === 'cardio') {
+ const parts = [];
+ if (entry.duration) parts.push(`${Math.floor(entry.duration / 60)} min`);
+ if (entry.distance) parts.push(`${entry.distance} km`);
+ if (entry.heart_rate) parts.push(`${entry.heart_rate} bpm`);
+ details = parts.join(' | ');
+ } else {
+ const parts = [];
+ if (entry.weight) parts.push(`${entry.weight} kg`);
+ if (entry.sets_completed) parts.push(`${entry.sets_completed} sets`);
+ if (entry.reps) parts.push(`${entry.reps} reps`);
+ details = parts.join(' x ');
+ }
+
+ html += `
+
+
+
${exercise.name || 'Exercise'}
+
${details}
+ ${entry.notes ? `
${entry.notes}
` : ''}
+
+
${exercise.type || 'strength'}
+
+ `;
+ });
+ } else {
+ html += '
No exercises logged
';
+ }
+
+ html += '
';
+
+ html += `
+
+
+
+ `;
+
+ document.getElementById('session-detail-content').innerHTML = html;
+ },
+
+ // Start new session
+ startNewSession() {
+ this.currentSession = null;
+ document.getElementById('session-form').reset();
+ document.getElementById('session-date').value = new Date().toISOString().slice(0, 16);
+ document.getElementById('session-entries-section').classList.add('hidden');
+ document.getElementById('session-entries').innerHTML = '';
+
+ // Populate plans dropdown
+ const planSelect = document.getElementById('session-plan');
+ planSelect.innerHTML = '' +
+ this.plans.map(p => ``).join('');
+
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
+ document.getElementById('view-log-session').classList.add('active');
+ },
+
+ // Save session
+ async saveSession() {
+ const date = document.getElementById('session-date').value;
+ const planId = document.getElementById('session-plan').value;
+ const notes = document.getElementById('session-notes').value;
+
+ const data = {
+ date: new Date(date).toISOString(),
+ notes: notes,
+ };
+ if (planId) {
+ data.plan_id = parseInt(planId);
+ }
+
+ try {
+ if (this.currentSession) {
+ this.currentSession = await api.updateSession(this.currentSession.id, data);
+ } else {
+ this.currentSession = await api.createSession(data);
+ }
+
+ // Show entries section
+ document.getElementById('session-entries-section').classList.remove('hidden');
+ this.renderSessionEntries();
+
+ // Reload sessions
+ this.sessions = await api.getSessions();
+ } catch (error) {
+ alert('Failed to save session');
+ }
+ },
+
+ // Render session entries
+ renderSessionEntries() {
+ const container = document.getElementById('session-entries');
+ const entries = this.currentSession?.entries || [];
+
+ if (entries.length === 0) {
+ container.innerHTML = 'No exercises yet. Add your first one!
';
+ return;
+ }
+
+ container.innerHTML = entries.map(entry => {
+ const exercise = entry.exercise || {};
+ let details = '';
+
+ if (exercise.type === 'cardio') {
+ const parts = [];
+ if (entry.duration) parts.push(`${Math.floor(entry.duration / 60)} min`);
+ if (entry.distance) parts.push(`${entry.distance} km`);
+ details = parts.join(' | ');
+ } else {
+ const parts = [];
+ if (entry.weight) parts.push(`${entry.weight} kg`);
+ if (entry.sets_completed) parts.push(`${entry.sets_completed} sets`);
+ if (entry.reps) parts.push(`${entry.reps} reps`);
+ details = parts.join(' x ');
+ }
+
+ return `
+
+
+
${exercise.name || 'Exercise'}
+
${details}
+
+
+ `;
+ }).join('');
+ },
+
+ // Show add entry form
+ showAddEntryForm() {
+ const select = document.getElementById('entry-exercise');
+ select.innerHTML = '' +
+ this.exercises.map(e => ``).join('');
+
+ document.getElementById('entry-form').reset();
+ document.getElementById('strength-fields').classList.remove('hidden');
+ document.getElementById('cardio-fields').classList.add('hidden');
+ document.getElementById('add-entry-modal').classList.remove('hidden');
+ },
+
+ // Hide add entry form
+ hideAddEntryForm() {
+ document.getElementById('add-entry-modal').classList.add('hidden');
+ },
+
+ // Update entry fields based on exercise type
+ updateEntryFields(exerciseId) {
+ const exercise = this.exercises.find(e => e.id === parseInt(exerciseId));
+ const isCardio = exercise && exercise.type === 'cardio';
+
+ document.getElementById('strength-fields').classList.toggle('hidden', isCardio);
+ document.getElementById('cardio-fields').classList.toggle('hidden', !isCardio);
+ },
+
+ // Save entry
+ async saveEntry() {
+ if (!this.currentSession) return;
+
+ const exerciseId = parseInt(document.getElementById('entry-exercise').value);
+ const exercise = this.exercises.find(e => e.id === exerciseId);
+
+ const data = {
+ exercise_id: exerciseId,
+ notes: document.getElementById('entry-notes').value,
+ };
+
+ if (exercise && exercise.type === 'cardio') {
+ const duration = parseInt(document.getElementById('entry-duration').value);
+ if (duration) data.duration = duration * 60; // convert to seconds
+ const distance = parseFloat(document.getElementById('entry-distance').value);
+ if (distance) data.distance = distance;
+ const hr = parseInt(document.getElementById('entry-hr').value);
+ if (hr) data.heart_rate = hr;
+ } else {
+ const weight = parseFloat(document.getElementById('entry-weight').value);
+ if (weight) data.weight = weight;
+ const sets = parseInt(document.getElementById('entry-sets').value);
+ if (sets) data.sets_completed = sets;
+ const reps = parseInt(document.getElementById('entry-reps').value);
+ if (reps) data.reps = reps;
+ }
+
+ try {
+ await api.addSessionEntry(this.currentSession.id, data);
+ this.currentSession = await api.getSession(this.currentSession.id);
+ this.renderSessionEntries();
+ this.hideAddEntryForm();
+ } catch (error) {
+ alert('Failed to add entry');
+ }
+ },
+
+ // Delete session
+ async deleteSession(id) {
+ if (!confirm('Are you sure you want to delete this session?')) return;
+
+ try {
+ await api.deleteSession(id);
+ this.sessions = await api.getSessions();
+ this.navigateTo('sessions');
+ } catch (error) {
+ alert('Failed to delete session');
+ }
+ },
+
+ // Render exercises list
+ renderExercises() {
+ const container = document.getElementById('exercises-list');
+ let filtered = this.exercises;
+
+ if (this.exerciseFilter !== 'all') {
+ filtered = this.exercises.filter(e => e.type === this.exerciseFilter);
+ }
+
+ if (filtered.length === 0) {
+ container.innerHTML = 'No exercises yet
';
+ return;
+ }
+
+ container.innerHTML = filtered.map(exercise => `
+
+
+ ${exercise.description ? `
${exercise.description}
` : ''}
+
+
+
+
+
+ `).join('');
+ },
+
+ // Show exercise form
+ showExerciseForm(exercise = null) {
+ document.getElementById('exercise-modal-title').textContent = exercise ? 'Edit Exercise' : 'Add Exercise';
+ document.getElementById('exercise-id').value = exercise ? exercise.id : '';
+ document.getElementById('exercise-name').value = exercise ? exercise.name : '';
+ document.getElementById('exercise-type').value = exercise ? exercise.type : 'strength';
+ document.getElementById('exercise-muscle').value = exercise ? exercise.muscle_group : '';
+ document.getElementById('exercise-description').value = exercise ? exercise.description : '';
+ document.getElementById('exercise-modal').classList.remove('hidden');
+ },
+
+ // Hide exercise form
+ hideExerciseForm() {
+ document.getElementById('exercise-modal').classList.add('hidden');
+ },
+
+ // Edit exercise
+ async editExercise(id) {
+ const exercise = this.exercises.find(e => e.id === id);
+ if (exercise) {
+ this.showExerciseForm(exercise);
+ }
+ },
+
+ // Save exercise
+ async saveExercise() {
+ const id = document.getElementById('exercise-id').value;
+ const data = {
+ name: document.getElementById('exercise-name').value,
+ type: document.getElementById('exercise-type').value,
+ muscle_group: document.getElementById('exercise-muscle').value,
+ description: document.getElementById('exercise-description').value,
+ };
+
+ try {
+ if (id) {
+ await api.updateExercise(id, data);
+ } else {
+ await api.createExercise(data);
+ }
+ this.exercises = await api.getExercises();
+ this.hideExerciseForm();
+ this.renderExercises();
+ } catch (error) {
+ alert('Failed to save exercise');
+ }
+ },
+
+ // Delete exercise
+ async deleteExercise(id) {
+ if (!confirm('Are you sure you want to delete this exercise?')) return;
+
+ try {
+ await api.deleteExercise(id);
+ this.exercises = await api.getExercises();
+ this.renderExercises();
+ } catch (error) {
+ alert('Failed to delete exercise');
+ }
+ },
+
+ // Render plans list
+ renderPlans() {
+ const container = document.getElementById('plans-list');
+
+ if (this.plans.length === 0) {
+ container.innerHTML = 'No training plans yet
';
+ return;
+ }
+
+ container.innerHTML = this.plans.map(plan => `
+
+ `).join('');
+ },
+
+ // View plan detail
+ async viewPlan(id) {
+ try {
+ this.currentPlan = await api.getPlan(id);
+ this.renderPlanDetail();
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
+ document.getElementById('view-plan-detail').classList.add('active');
+ } catch (error) {
+ alert('Failed to load plan');
+ }
+ },
+
+ // Render plan detail
+ renderPlanDetail() {
+ const plan = this.currentPlan;
+ if (!plan) return;
+
+ document.getElementById('plan-detail-title').textContent = plan.name;
+
+ let html = '';
+ if (plan.description) {
+ html += `${plan.description}
`;
+ }
+
+ html += 'Exercises
';
+
+ if (plan.exercises && plan.exercises.length > 0) {
+ plan.exercises.forEach((pe, index) => {
+ const exercise = pe.exercise || {};
+ let details = [];
+ if (pe.sets) details.push(`${pe.sets} sets`);
+ if (pe.reps) details.push(`${pe.reps} reps`);
+ if (pe.duration) details.push(`${Math.floor(pe.duration / 60)} min`);
+
+ html += `
+
+
+
${index + 1}. ${exercise.name || 'Exercise'}
+
${details.join(' x ') || 'No targets set'}
+
+
${exercise.type || 'strength'}
+
+ `;
+ });
+ } else {
+ html += '
No exercises in this plan
';
+ }
+
+ html += '
';
+
+ html += `
+
+
+
+ `;
+
+ document.getElementById('plan-detail-content').innerHTML = html;
+ },
+
+ // Start session from plan
+ startSessionFromPlan(planId) {
+ this.startNewSession();
+ document.getElementById('session-plan').value = planId;
+ },
+
+ // Show plan form
+ showPlanForm(plan = null) {
+ document.getElementById('plan-modal-title').textContent = plan ? 'Edit Plan' : 'Add Training Plan';
+ document.getElementById('plan-id').value = plan ? plan.id : '';
+ document.getElementById('plan-name').value = plan ? plan.name : '';
+ document.getElementById('plan-description').value = plan ? plan.description : '';
+
+ const exercisesList = document.getElementById('plan-exercises-list');
+ exercisesList.innerHTML = '';
+
+ if (plan && plan.exercises) {
+ plan.exercises.forEach(pe => this.addPlanExerciseRow(pe));
+ }
+
+ document.getElementById('plan-modal').classList.remove('hidden');
+ },
+
+ // Hide plan form
+ hidePlanForm() {
+ document.getElementById('plan-modal').classList.add('hidden');
+ },
+
+ // Add plan exercise row
+ addPlanExercise() {
+ this.addPlanExerciseRow();
+ },
+
+ addPlanExerciseRow(data = null) {
+ const container = document.getElementById('plan-exercises-list');
+ const index = container.children.length;
+
+ const div = document.createElement('div');
+ div.className = 'plan-exercise-item';
+ div.innerHTML = `
+
+
+
+
+ `;
+ container.appendChild(div);
+ },
+
+ // Edit plan
+ async editPlan(id) {
+ try {
+ const plan = await api.getPlan(id);
+ this.showPlanForm(plan);
+ } catch (error) {
+ alert('Failed to load plan');
+ }
+ },
+
+ // Save plan
+ async savePlan() {
+ const id = document.getElementById('plan-id').value;
+ const exercises = [];
+
+ document.querySelectorAll('#plan-exercises-list .plan-exercise-item').forEach((item, index) => {
+ const exerciseId = item.querySelector('[name="exercise_id"]').value;
+ const sets = item.querySelector('[name="sets"]').value;
+ const reps = item.querySelector('[name="reps"]').value;
+
+ if (exerciseId) {
+ exercises.push({
+ exercise_id: parseInt(exerciseId),
+ sets: sets ? parseInt(sets) : 0,
+ reps: reps ? parseInt(reps) : 0,
+ order: index,
+ });
+ }
+ });
+
+ const data = {
+ name: document.getElementById('plan-name').value,
+ description: document.getElementById('plan-description').value,
+ exercises: exercises,
+ };
+
+ try {
+ if (id) {
+ await api.updatePlan(id, data);
+ } else {
+ await api.createPlan(data);
+ }
+ this.plans = await api.getPlans();
+ this.hidePlanForm();
+ this.renderPlans();
+ } catch (error) {
+ alert('Failed to save plan');
+ }
+ },
+
+ // Delete plan
+ async deletePlan(id) {
+ if (!confirm('Are you sure you want to delete this plan?')) return;
+
+ try {
+ await api.deletePlan(id);
+ this.plans = await api.getPlans();
+ this.renderPlans();
+ } catch (error) {
+ alert('Failed to delete plan');
+ }
+ },
+};
+
+// Initialize app when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+ app.init();
+});