Download index.html from kinda12/runsync: direct link, hf CLI and curl.
- Browser
- Download file 98.8 kB
-
https://huggingface.co/spaces/kinda12/runsync/resolve/main/index.html
- Command line
-
hf download hf://spaces/kinda12/runsync/index.html
-
curl -L -o index.html https://huggingface.co/spaces/kinda12/runsync/resolve/main/index.html
98.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RunSync | Advanced Garmin Running Plan Integration</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Firebase SDKs --> | |
| <script type="module"> | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"; | |
| import { getAuth, signInAnonymously, signInWithCustomToken, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"; | |
| import { getFirestore, doc, getDoc, setDoc, collection, query, where, onSnapshot, updateDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"; | |
| let app; | |
| let db; | |
| let auth; | |
| let userId; // Global to store the current user ID | |
| let userAuthReady = false; // Flag to ensure auth is ready before Firestore ops | |
| // Firebase configuration and initialization | |
| const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // Fallback for local testing | |
| if (Object.keys(firebaseConfig).length > 0) { | |
| app = initializeApp(firebaseConfig); | |
| db = getFirestore(app); | |
| auth = getAuth(app); | |
| // Authentication listener | |
| onAuthStateChanged(auth, async (user) => { | |
| // Ensure elements exist before trying to modify their classList | |
| const loggedInProfile = document.getElementById('logged-in-profile'); | |
| const loginButton = document.getElementById('login-button'); | |
| const logoutButton = document.getElementById('logout-button'); | |
| const userNameDisplay = document.getElementById('user-name-display'); | |
| const authStatus = document.getElementById('auth-status'); | |
| const mobileUserNameDisplay = document.getElementById('mobile-user-name-display'); // Added this | |
| if (user) { | |
| userId = user.uid; | |
| if (userNameDisplay) userNameDisplay.textContent = user.displayName || user.email || `User: ${userId.substring(0, 8)}...`; | |
| if (mobileUserNameDisplay) mobileUserNameDisplay.textContent = user.displayName || user.email || `User: ${userId.substring(0, 8)}...`; // Update mobile display | |
| if (authStatus) authStatus.textContent = 'Logged In'; | |
| if (loginButton) loginButton.classList.add('hidden'); | |
| if (logoutButton) logoutButton.classList.remove('hidden'); | |
| if (loggedInProfile) loggedInProfile.classList.remove('hidden'); // Ensure this element exists | |
| } else { | |
| // Sign in anonymously if no user is logged in | |
| if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { | |
| try { | |
| await signInWithCustomToken(auth, __initial_auth_token); | |
| } catch (error) { | |
| console.error("Error signing in with custom token:", error); | |
| await signInAnonymously(auth); // Fallback to anonymous | |
| } | |
| } else { | |
| await signInAnonymously(auth); | |
| } | |
| userId = auth.currentUser?.uid || crypto.randomUUID(); // Fallback if anonymous fails too | |
| if (userNameDisplay) userNameDisplay.textContent = `Guest: ${userId.substring(0, 8)}...`; | |
| if (mobileUserNameDisplay) mobileUserNameDisplay.textContent = `Guest: ${userId.substring(0, 8)}...`; // Update mobile display | |
| if (authStatus) authStatus.textContent = 'Guest Mode'; | |
| if (loginButton) loginButton.classList.remove('hidden'); | |
| if (logoutButton) logoutButton.classList.add('hidden'); | |
| if (loggedInProfile) loggedInProfile.classList.add('hidden'); // Ensure this element exists | |
| } | |
| userAuthReady = true; | |
| // Once auth is ready, try to load user data/plan | |
| loadUserData(); | |
| }); | |
| } else { | |
| console.warn("Firebase config not found. Running in demo mode without persistence."); | |
| // Simulate anonymous user for demo mode | |
| userId = crypto.randomUUID(); | |
| const userNameDisplay = document.getElementById('user-name-display'); | |
| const authStatus = document.getElementById('auth-status'); | |
| const loginButton = document.getElementById('login-button'); | |
| const logoutButton = document.getElementById('logout-button'); | |
| const loggedInProfile = document.getElementById('logged-in-profile'); | |
| const mobileUserNameDisplay = document.getElementById('mobile-user-name-display'); | |
| if (userNameDisplay) userNameDisplay.textContent = `Guest: ${userId.substring(0, 8)}...`; | |
| if (mobileUserNameDisplay) mobileUserNameDisplay.textContent = `Guest: ${userId.substring(0, 8)}...`; | |
| if (authStatus) authStatus.textContent = 'Guest Mode (No Persistence)'; | |
| if (loginButton) loginButton.classList.remove('hidden'); | |
| if (logoutButton) logoutButton.classList.add('hidden'); | |
| if (loggedInProfile) loggedInProfile.classList.add('hidden'); | |
| userAuthReady = true; // Mark as ready for local demo | |
| } | |
| // --- Firestore Functions --- | |
| async function saveUserData(data) { | |
| if (!userAuthReady || !db || !userId) { | |
| console.warn("Firebase not ready or userId missing. Cannot save data."); | |
| return; | |
| } | |
| try { | |
| // Private data for the user | |
| const userDocRef = doc(db, `artifacts/${appId}/users/${userId}/profile`, 'data'); | |
| await setDoc(userDocRef, data, { merge: true }); | |
| console.log("User data saved successfully!"); | |
| } catch (e) { | |
| console.error("Error saving user data: ", e); | |
| } | |
| } | |
| async function loadUserData() { | |
| if (!userAuthReady || !db || !userId) { | |
| console.warn("Firebase not ready or userId missing. Cannot load data."); | |
| return null; | |
| } | |
| try { | |
| // Private data for the user | |
| const userDocRef = doc(db, `artifacts/${appId}/users/${userId}/profile`, 'data'); | |
| const docSnap = await getDoc(userDocRef); | |
| if (docSnap.exists()) { | |
| console.log("User data loaded:", docSnap.data()); | |
| // This is where you would restore the user's plan if it was saved | |
| const savedPlan = docSnap.data().currentPlan; | |
| if (savedPlan) { | |
| displayGeneratedPlan(savedPlan); | |
| document.getElementById('plan-form').closest('div').classList.add('hidden'); | |
| document.getElementById('plan-display').classList.remove('hidden'); | |
| } | |
| return docSnap.data(); | |
| } else { | |
| console.log("No user data found."); | |
| return null; | |
| } | |
| } catch (e) { | |
| console.error("Error loading user data: ", e); | |
| return null; | |
| } | |
| } | |
| // Expose functions to global scope for use in main script | |
| window.saveUserData = saveUserData; | |
| window.loadUserData = loadUserData; | |
| // userId and auth will be updated by onAuthStateChanged in the module scope | |
| // It's safer to rely on the onAuthStateChanged callback for the most up-to-date userId and auth instance | |
| </script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#009EE3', | |
| secondary: '#FF6500', | |
| dark: '#111827', | |
| light: '#F9FAFB', | |
| accent: '#00C389' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| scroll-behavior: smooth; | |
| background: linear-gradient(180deg, #f0f9ff 0%, #ffffff 100%); | |
| } | |
| .workout-card { | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 6px rgba(0, 158, 227, 0.1); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .workout-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 15px rgba(0, 158, 227, 0.2); | |
| } | |
| .calendar-day { | |
| transition: all 0.2s ease; | |
| } | |
| .calendar-day.selected { | |
| border-color: #009EE3; | |
| transform: scale(1.05); | |
| } | |
| .pace-selector input[type="radio"]:checked + label { | |
| border-color: #009EE3; | |
| background-color: rgba(0, 158, 227, 0.1); | |
| transform: translateY(-3px); | |
| box-shadow: 0 5px 10px rgba(0, 158, 227, 0.15); | |
| } | |
| .fitness-level input[type="radio"]:checked + label { | |
| border-color: #009EE3; /* primary color */ | |
| background-color: rgba(0, 158, 227, 0.05); /* very light primary */ | |
| transform: translateY(-3px); | |
| box-shadow: 0 5px 10px rgba(0, 158, 227, 0.1); | |
| } | |
| .tooltip-container:hover .tooltip { | |
| display: block; | |
| } | |
| .garmin-badge { | |
| background: linear-gradient(135deg, #009EE3 0%, #005E8C 100%); | |
| } | |
| .progress-bar { | |
| background: linear-gradient(to right, #00C389, #009EE3); | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(0, 158, 227, 0.4); } | |
| 70% { box-shadow: 0 0 0 10px rgba(0, 158, 227, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(0, 158, 227, 0); } | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes float { | |
| 0% { transform: translateY(0px); } | |
| 50% { transform: translateY(-10px); } | |
| 100% { transform: translateY(0px); } | |
| } | |
| .floating { | |
| animation: float 4s ease-in-out infinite; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.7); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .calendar-day.has-workout { | |
| position: relative; | |
| overflow: visible; | |
| } | |
| .calendar-day.has-workout::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 4px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 6px; | |
| height: 6px; | |
| background-color: #009EE3; | |
| border-radius: 50%; | |
| } | |
| .streak-container { | |
| background: linear-gradient(120deg, #009EE3, #00C389); | |
| transform: rotate(-2deg); | |
| border-radius: 16px; | |
| margin-top: -10px; | |
| margin-bottom: 10px; | |
| } | |
| .streak-box { | |
| transform: rotate(2deg); | |
| padding: 20px; | |
| } | |
| #mobile-menu { | |
| transition: all 0.3s ease; | |
| } | |
| .dark-gradient { | |
| background: linear-gradient(135deg, #111827, #1e293b); | |
| } | |
| .feature-icon { | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 10px rgba(0, 195, 137, 0.15); | |
| } | |
| .feature-card:hover .feature-icon { | |
| transform: scale(1.1); | |
| background: linear-gradient(135deg, #009EE3, #00C389); | |
| } | |
| .badge-animate { | |
| animation: heartbeat 1.5s infinite; | |
| } | |
| @keyframes heartbeat { | |
| 0% { transform: scale(1); } | |
| 14% { transform: scale(1.1); } | |
| 28% { transform: scale(1); } | |
| 42% { transform: scale(1.1); } | |
| 70% { transform: scale(1); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-800"> | |
| <!-- Header/Navigation --> | |
| <nav class="bg-white shadow-sm sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between h-16"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 flex items-center"> | |
| <i class="fas fa-running text-primary text-2xl"></i> | |
| <span class="ml-2 text-xl font-bold text-primary">RunSync</span> | |
| </div> | |
| <div class="hidden md:ml-6 md:flex md:space-x-8"> | |
| <a href="#" class="border-b-2 border-primary px-1 pt-1 text-sm font-medium text-gray-900">Plan</a> | |
| <a href="#" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 border-b-2 px-1 pt-1 text-sm font-medium">Calendar</a> | |
| <a href="#" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 border-b-2 px-1 pt-1 text-sm font-medium">Progress</a> | |
| <a href="#" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 border-b-2 px-1 pt-1 text-sm font-medium">Community</a> | |
| </div> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="hidden md:flex items-center space-x-4"> | |
| <button class="text-sm font-medium text-gray-700 hover:text-primary flex items-center"> | |
| <i class="far fa-bell mr-1"></i> | |
| <span class="absolute -mt-4 ml-3 w-2 h-2 bg-red-500 rounded-full"></span> | |
| </button> | |
| <button class="text-sm font-medium text-gray-700 hover:text-primary"> | |
| <i class="fas fa-trophy"></i> | |
| </button> | |
| <!-- Added this div to wrap the user profile elements --> | |
| <div id="logged-in-profile" class="hidden flex items-center space-x-2"> | |
| <div class="relative"> | |
| <button type="button" class="flex rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2" id="user-menu-button"> | |
| <span class="sr-only">Open user menu</span> | |
| <img class="h-8 w-8 rounded-full border-2 border-primary" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""> | |
| </button> | |
| </div> | |
| <span id="user-name-display" class="text-sm font-medium text-gray-700">Loading...</span> | |
| <span id="auth-status" class="text-xs text-gray-500"></span> | |
| </div> | |
| <button id="login-button" class="px-3 py-1 border border-primary text-primary rounded-md text-sm hover:bg-primary hover:text-white transition hidden">Login</button> | |
| <button id="logout-button" class="px-3 py-1 border border-gray-300 text-gray-700 rounded-md text-sm hover:bg-gray-100 transition hidden">Logout</button> | |
| </div> | |
| <button id="mobile-menu-button" class="md:hidden text-gray-500"> | |
| <i class="fas fa-bars text-xl"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mobile menu --> | |
| <div id="mobile-menu" class="hidden md:hidden bg-white shadow-lg absolute w-full z-50"> | |
| <div class="px-2 pt-2 pb-3 space-y-1 sm:px-3"> | |
| <a href="#" class="text-gray-900 block px-3 py-2 rounded-md text-base font-medium bg-gray-50">Plan</a> | |
| <a href="#" class="text-gray-700 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Calendar</a> | |
| <a href="#" class="text-gray-700 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Progress</a> | |
| <a href="#" class="text-gray-700 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Community</a> | |
| <div class="border-t pt-2 flex justify-between items-center"> | |
| <div class="flex items-center px-3 py-2"> | |
| <img class="h-8 w-8 rounded-full border-2 border-primary" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""> | |
| <span id="mobile-user-name-display" class="ml-2 text-sm font-medium text-gray-900">John Doe</span> | |
| </div> | |
| <button class="text-gray-500 px-4 py-2"> | |
| <i class="fas fa-cog text-xl"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Hero Section --> | |
| <div class="bg-gradient-to-r from-primary to-[#0080b3] relative overflow-hidden"> | |
| <div class="absolute inset-0"> | |
| <div class="absolute inset-y-0 right-0 -mr-32 w-2/3 opacity-20"> | |
| <div class="grid grid-cols-5 grid-rows-8 gap-4 h-full"> | |
| <div class="bg-blue-400 rounded-full transform rotate-45"></div> | |
| <div class="bg-blue-300 rounded-full transform rotate-12"></div> | |
| <div class="bg-blue-200 rounded-full"></div> | |
| <div class="bg-blue-300 rounded-full transform -rotate-12"></div> | |
| <div class="bg-blue-400 rounded-full transform rotate-45"></div> | |
| <div class="bg-blue-200 rounded-full transform -rotate-30"></div> | |
| <div class="bg-blue-400 rounded-full transform rotate-12"></div> | |
| <div class="bg-blue-300 rounded-full"></div> | |
| <div class="bg-blue-200 rounded-full transform rotate-30"></div> | |
| <div class="bg-blue-400 rounded-full"></div> | |
| <!-- More elements repeating pattern --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="max-w-7xl mx-auto py-16 px-4 sm:px-6 lg:py-24 lg:px-8 lg:flex lg:items-center lg:justify-between relative"> | |
| <div class="flex-1 min-w-0 z-10"> | |
| <div class="inline-block bg-white/20 py-1 px-4 rounded-full mb-4"> | |
| <span class="text-white text-sm font-medium">NEW: Race Day Predictor Tool</span> | |
| </div> | |
| <h1 class="text-4xl font-extrabold tracking-tight text-white sm:text-5xl lg:text-6xl"> | |
| <span class="block">Run Your Best Race</span> | |
| <span class="block text-primary-200 font-light">Personalized Plan, Garmin Sync</span> | |
| </h1> | |
| <p class="mt-6 max-w-3xl text-xl text-white"> | |
| Create science-based 80/20 training plans for any distance. Sync directly to your Garmin watch and track progress automatically. | |
| </p> | |
| <div class="mt-8 flex flex-col sm:flex-row sm:space-y-0 sm:space-x-6 space-y-4"> | |
| <div> | |
| <a href="#plan-creator" class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-md text-white bg-secondary hover:bg-orange-600 transition duration-300 transform hover:-translate-y-1"> | |
| Create My Plan <i class="fas fa-chevron-down ml-2"></i> | |
| </a> | |
| </div> | |
| <div> | |
| <button id="connect-garmin-hero" class="inline-flex items-center justify-center px-6 py-3 border-2 border-white text-base font-medium rounded-md shadow-sm text-white bg-transparent hover:bg-white/20 transition duration-300"> | |
| <i class="fab fa-garmin mr-2"></i> Connect Garmin | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-12 lg:mt-0 z-10"> | |
| <div class="relative floating"> | |
| <img class="w-full max-w-lg" src="https://cdn.pixabay.com/photo/2014/03/25/16/24/silhouette-297348_1280.png" alt="Runner silhouette"> | |
| <div class="absolute bottom-10 -right-10 bg-white rounded-lg shadow-lg p-4 w-60 transform rotate-6"> | |
| <div class="flex justify-between items-center"> | |
| <span class="font-medium">Current streak</span> | |
| <span class="bg-primary text-white text-xs font-bold px-2 py-1 rounded-full">3 days</span> | |
| </div> | |
| <div class="mt-2 flex space-x-1"> | |
| <div class="w-6 h-6 bg-green-400 rounded-full"></div> | |
| <div class="w-6 h-6 bg-green-400 rounded-full"></div> | |
| <div class="w-6 h-6 bg-green-400 rounded-full"></div> | |
| <div class="w-6 h-6 bg-gray-200 rounded-full"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Featured Highlights --> | |
| <div class="py-12 bg-white"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
| <div class="feature-card bg-gradient-to-br from-blue-50 to-white p-6 rounded-xl shadow-sm border border-blue-100 flex items-center transition-all duration-300 hover:shadow-md"> | |
| <div class="feature-icon w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-white text-xl mr-4"> | |
| <i class="fas fa-dumbbell"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold text-gray-900">80/20 Principle</h3> | |
| <p class="mt-2 text-gray-600">Optimal balance between easy and hard running</p> | |
| </div> | |
| </div> | |
| <div class="feature-card bg-gradient-to-br from-green-50 to-white p-6 rounded-xl shadow-sm border border-green-100 flex items-center transition-all duration-300 hover:shadow-md"> | |
| <div class="feature-icon w-12 h-12 bg-accent rounded-xl flex items-center justify-center text-white text-xl mr-4"> | |
| <i class="fas fa-sync-alt"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold text-gray-900">Smart Auto Sync</h3> | |
| <p class="mt-2 text-gray-600">Workouts appear on your watch automatically</p> | |
| </div> | |
| </div> | |
| <div class="feature-card bg-gradient-to-br from-yellow-50 to-white p-6 rounded-xl shadow-sm border border-yellow-100 flex items-center transition-all duration-300 hover:shadow-md"> | |
| <div class="feature-icon w-12 h-12 bg-secondary rounded-xl flex items-center justify-center text-white text-xl mr-4"> | |
| <i class="fas fa-chart-line"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold text-gray-900">Progress Analysis</h3> | |
| <p class="mt-2 text-gray-600">Get detailed insights on your improvements</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Streak & Motivation --> | |
| <div class="py-8 relative overflow-hidden"> | |
| <div class="streak-container w-full py-2"> | |
| <div class="streak-box text-center"> | |
| <div class="flex justify-center items-center"> | |
| <div class="bg-yellow-400 text-yellow-900 text-sm px-3 py-1 rounded-full font-semibold flex items-center badge-animate"> | |
| <i class="fas fa-fire mr-1"></i> <span id="streak-display">14-day streak</span> | |
| </div> | |
| <div class="ml-4 text-white font-medium">Your best streak: <span id="best-streak-display">28 days</span></div> | |
| </div> | |
| <div class="mt-2 text-blue-100 font-light"> | |
| "The miracle isn't that I finished. The miracle is that I had the courage to start." - John Bingham | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Plan Creation Section --> | |
| <div id="plan-creator" class="py-12 bg-white"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="lg:text-center"> | |
| <h2 class="text-base text-primary font-semibold tracking-wide uppercase">Build Your Training</h2> | |
| <p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl"> | |
| Create Your Running Plan | |
| </p> | |
| <p class="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto"> | |
| Configure your training parameters below. We'll generate a personalized plan following the 80/20 rule with two quality runs per week. | |
| </p> | |
| </div> | |
| <div class="mt-10 bg-gradient-to-b from-gray-50 to-gray-100 rounded-2xl shadow-lg p-8"> | |
| <form id="plan-form" class="space-y-8"> | |
| <!-- Plan Parameters --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
| <div class="col-span-1"> | |
| <label for="race-distance" class="block text-sm font-medium text-gray-700 mb-1 flex items-center"> | |
| Target Race Distance | |
| <span class="tooltip-container ml-1 relative"> | |
| <i class="fas fa-info-circle text-gray-400 hover:text-gray-600 cursor-pointer"></i> | |
| <div class="tooltip absolute z-10 bottom-full mb-2 w-64 p-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm transition-all" role="tooltip"> | |
| Select the distance you're training for. Your plan will be adjusted accordingly. | |
| </div> | |
| </span> | |
| </label> | |
| <select id="race-distance" class="mt-1 block w-full pl-3 pr-10 py-3 text-base border-2 border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary sm:text-sm rounded-lg transition duration-200"> | |
| <option value="5">5K (3.1 miles)</option> | |
| <option value="10">10K (6.2 miles)</option> | |
| <option value="13.1">Half Marathon (13.1 miles)</option> | |
| <option value="26.2" selected>Marathon (26.2 miles)</option> | |
| </select> | |
| </div> | |
| <div class="col-span-1"> | |
| <label for="weeks" class="block text-sm font-medium text-gray-700 mb-1 flex items-center"> | |
| Training Duration | |
| <span class="tooltip-container ml-1 relative"> | |
| <i class="fas fa-info-circle text-gray-400 hover:text-gray-600 cursor-pointer"></i> | |
| <div class="tooltip absolute z-10 bottom-full mb-2 w-64 p-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm" role="tooltip"> | |
| Recommended: 16-20 weeks for marathon, 8-12 weeks for 5K | |
| </div> | |
| </span> | |
| </label> | |
| <div class="relative"> | |
| <input type="number" id="weeks" min="6" max="20" value="16" class="mt-1 block w-full pl-3 pr-16 py-3 text-base border-2 border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary sm:text-sm rounded-lg"> | |
| <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> | |
| <span class="text-gray-500 sm:text-sm">weeks</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 text-xs text-gray-500">Adjustment: <span id="duration-status">Optimal</span></div> | |
| </div> | |
| <div class="col-span-1"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1"> | |
| Measurement Units | |
| </label> | |
| <div class="flex space-x-4 mt-1"> | |
| <div class="flex items-center"> | |
| <input id="units-miles" name="units" type="radio" value="miles" checked class="focus:ring-primary h-4 w-4 text-primary border-gray-300"> | |
| <label for="units-miles" class="ml-2 block text-sm text-gray-700">Miles</label> | |
| </div> | |
| <div class="flex items-center"> | |
| <input id="units-km" name="units" type="radio" value="km" class="focus:ring-primary h-4 w-4 text-primary border-gray-300"> | |
| <label for="units-km" class="ml-2 block text-sm text-gray-700">Kilometers</label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Current Fitness Level --> | |
| <div class="border-t border-gray-200 pt-6"> | |
| <div class="flex items-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Current Fitness Level</h3> | |
| <span class="tooltip-container ml-2 relative"> | |
| <i class="fas fa-info-circle text-gray-400 hover:text-gray-600 cursor-pointer"></i> | |
| <div class="tooltip absolute z-10 bottom-full mb-2 w-64 p-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm" role="tooltip"> | |
| Helps us set appropriate baseline distances and intensities for your plan. | |
| </div> | |
| </span> | |
| </div> | |
| <div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-4"> | |
| <div class="fitness-level"> | |
| <input type="radio" id="fitness-beginner" name="fitness" value="Beginner" class="hidden"> | |
| <label for="fitness-beginner" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-200 hover:bg-blue-50"> | |
| <span class="block font-medium text-gray-800">Beginner</span> | |
| <span class="block text-sm text-gray-500 mt-1">New to structured training</span> | |
| </label> | |
| </div> | |
| <div class="fitness-level"> | |
| <input type="radio" id="fitness-intermediate" name="fitness" value="Intermediate" checked class="hidden"> | |
| <label for="fitness-intermediate" class="block px-4 py-3 border-2 border-blue-300 bg-blue-50 rounded-lg text-center cursor-pointer transition duration-200 hover:shadow"> | |
| <span class="block font-medium text-gray-800">Intermediate</span> | |
| <span class="block text-sm text-gray-500 mt-1">Regular runner, some experience</span> | |
| </label> | |
| </div> | |
| <div class="fitness-level"> | |
| <input type="radio" id="fitness-advanced" name="fitness" value="Advanced" class="hidden"> | |
| <label for="fitness-advanced" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-200 hover:bg-blue-50"> | |
| <span class="block font-medium text-gray-800">Advanced</span> | |
| <span class="block text-sm text-gray-500 mt-1">Seeking performance gains</span> | |
| </label> | |
| </div> | |
| <div class="fitness-level"> | |
| <input type="radio" id="fitness-elite" name="fitness" value="Elite" class="hidden"> | |
| <label for="fitness-elite" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-200 hover:bg-blue-50"> | |
| <span class="block font-medium text-gray-800">Elite</span> | |
| <span class="block text-sm text-gray-500 mt-1">Competitive level training</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Weekly Mileage --> | |
| <div class="border-t border-gray-200 pt-6"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Weekly Mileage Goal</h3> | |
| <span id="mileage-display" class="text-xl font-semibold text-primary">30 miles</span> | |
| </div> | |
| <div class="mt-4"> | |
| <input type="range" id="mileage" min="20" max="70" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>20</span> | |
| <span>30</span> | |
| <span>40</span> | |
| <span>50</span> | |
| <span>60</span> | |
| <span>70+</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 text-xs text-gray-500"> | |
| <span id="mileage-note">Optimal for marathon training</span> | |
| </div> | |
| </div> | |
| <!-- Goal Pace --> | |
| <div class="border-t border-gray-200 pt-6"> | |
| <div class="flex items-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Goal Race Pace</h3> | |
| <span class="tooltip-container ml-2 relative"> | |
| <i class="fas fa-info-circle text-gray-400 hover:text-gray-600 cursor-pointer"></i> | |
| <div class="tooltip absolute z-10 bottom-full mb-2 w-64 p-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm" role="tooltip"> | |
| Your target pace for race day (minutes per mile or per kilometer) | |
| </div> | |
| </span> | |
| </div> | |
| <div class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div class="pace-selector"> | |
| <input type="radio" id="pace-easy" name="pace" value="10:00-12:00" class="hidden"> | |
| <label for="pace-easy" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-300"> | |
| <span class="block text-sm font-medium text-gray-700">Slow</span> | |
| <span class="block text-xs text-gray-500 mt-1">10:00-12:00 min/mile</span> | |
| </label> | |
| </div> | |
| <div class="pace-selector"> | |
| <input type="radio" id="pace-moderate" name="pace" value="8:30-9:59" checked class="hidden"> | |
| <label for="pace-moderate" class="block px-4 py-3 border-2 border-primary bg-blue-50 rounded-lg text-center cursor-pointer transition duration-200"> | |
| <span class="block text-sm font-medium text-gray-700">Moderate</span> | |
| <span class="block text-xs text-gray-500 mt-1">8:30-9:59 min/mile</span> | |
| </label> | |
| </div> | |
| <div class="pace-selector"> | |
| <input type="radio" id="pace-fast" name="pace" value="7:00-8:29" class="hidden"> | |
| <label for="pace-fast" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-300"> | |
| <span class="block text-sm font-medium text-gray-700">Fast</span> | |
| <span class="block text-xs text-gray-500 mt-1">7:00-8:29 min/mile</span> | |
| </label> | |
| </div> | |
| <div class="pace-selector"> | |
| <input type="radio" id="pace-competitive" name="pace" value="6:00-6:59" class="hidden"> | |
| <label for="pace-competitive" class="block px-4 py-3 border-2 border-gray-200 rounded-lg text-center cursor-pointer transition duration-200 hover:border-blue-300"> | |
| <span class="block text-sm font-medium text-gray-700">Competitive</span> | |
| <span class="block text-xs text-gray-500 mt-1">6:00-6:59 min/mile</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="border-t border-gray-200 pt-6 flex justify-center"> | |
| <button id="generate-plan" type="button" class="px-8 py-4 border border-transparent text-base font-medium rounded-xl shadow-md text-white bg-gradient-to-r from-primary to-blue-700 hover:from-blue-600 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-300 transform hover:-translate-y-1 w-full md:w-auto"> | |
| <i class="fas fa-bolt mr-2"></i> Generate Training Plan | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Training Plan Preview --> | |
| <div class="py-12 bg-gradient-to-b from-blue-50 to-gray-100"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="text-center"> | |
| <h2 class="text-3xl font-bold text-gray-900 mb-2">How Your Plan Will Look</h2> | |
| <p class="text-xl text-gray-600 max-w-2xl mx-auto">Sample structure from a typical marathon training plan</p> | |
| </div> | |
| <div class="mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden transition-transform duration-300 hover:-translate-y-2"> | |
| <div class="bg-gradient-to-r from-blue-500 to-blue-700 py-4 px-6"> | |
| <h3 class="text-lg font-semibold text-white">Weekly Pattern</h3> | |
| </div> | |
| <div class="p-6"> | |
| <ul class="space-y-3"> | |
| <li class="flex items-start"> | |
| <i class="fas fa-shoe-prints text-accent mt-1 mr-3"></i> | |
| <span class="flex-1">2 quality workouts per week</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-running text-secondary mt-1 mr-3"></i> | |
| <span class="flex-1">1 long run per week</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-walking text-gray-400 mt-1 mr-3"></i> | |
| <span class="flex-1">3-4 easy runs</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-couch text-gray-400 mt-1 mr-3"></i> | |
| <span class="flex-1">1-2 rest days</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden transition-transform duration-300 hover:-translate-y-2"> | |
| <div class="bg-gradient-to-r from-accent to-emerald-700 py-4 px-6"> | |
| <h3 class="text-lg font-semibold text-white">Quality Workouts</h3> | |
| </div> | |
| <div class="p-6"> | |
| <ul class="space-y-3"> | |
| <li class="flex items-start"> | |
| <i class="fas fa-tachometer-alt text-blue-500 mt-1 mr-3"></i> | |
| <span class="flex-1">Tempo runs at goal pace</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-stopwatch text-purple-500 mt-1 mr-3"></i> | |
| <span class="flex-1">Interval training for speed</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-mountain text-gray-700 mt-1 mr-3"></i> | |
| <span class="flex-1">Hill repeats for strength</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden transition-transform duration-300 hover:-translate-y-2"> | |
| <div class="bg-gradient-to-r from-secondary to-orange-700 py-4 px-6"> | |
| <h3 class="text-lg font-semibold text-white">Progress Milestones</h3> | |
| </div> | |
| <div class="p-6"> | |
| <ul class="space-y-3"> | |
| <li class="flex items-start"> | |
| <i class="fas fa-calendar-check text-secondary mt-1 mr-3"></i> | |
| <span class="flex-1">Phase 1: Base building</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-chart-line text-accent mt-1 mr-3"></i> | |
| <span class="flex-1">Phase 2: Build phase</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-trophy text-yellow-500 mt-1 mr-3"></i> | |
| <span class="flex-1">Phase 3: Taper & race</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden transition-transform duration-300 hover:-translate-y-2"> | |
| <div class="bg-gradient-to-r from-purple-500 to-purple-700 py-4 px-6"> | |
| <h3 class="text-lg font-semibold text-white">Auto Adjustments</h3> | |
| </div> | |
| <div class="p-6"> | |
| <ul class="space-y-3"> | |
| <li class="flex items-start"> | |
| <i class="fas fa-heartbeat text-red-500 mt-1 mr-3"></i> | |
| <span class="flex-1">Performance-based adaptations</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-balance-scale text-gray-600 mt-1 mr-3"></i> | |
| <span class="flex-1">Injury prevention balancing</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i class="fas fa-umbrella-beach text-yellow-500 mt-1 mr-3"></i> | |
| <span class="flex-1">Schedule conflicts auto-reschedule</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Plan Display Section --> | |
| <div id="plan-display" class="py-12 bg-gray-50 hidden"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8"> | |
| <div> | |
| <h2 id="plan-title" class="text-2xl font-bold text-gray-900">Your Training Plan</h2> | |
| <p id="plan-summary" class="mt-2 text-gray-600">Loading plan details...</p> | |
| </div> | |
| <div class="mt-4 md:mt-0 flex flex-wrap gap-3"> | |
| <button type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" onclick="showMessage('Download Calendar', 'Download Calendar functionality needs backend to generate ICS file.')"> | |
| <i class="far fa-calendar-alt mr-2"></i> Download Calendar | |
| </button> | |
| <button id="sync-to-garmin-button" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-blue-700"> | |
| <i class="fab fa-garmin mr-2"></i> Sync to Garmin | |
| </button> | |
| <button id="calendar-toggle" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"> | |
| <i class="far fa-calendar mr-2"></i> Calendar View | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-8 bg-white rounded-lg shadow p-6"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Plan Overview</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-4"> | |
| <div class="bg-blue-50 p-4 rounded-md border border-blue-100"> | |
| <p class="text-sm font-medium text-gray-500">Total weeks</p> | |
| <p id="total-weeks-display" class="text-2xl font-bold">--</p> | |
| </div> | |
| <div class="bg-green-50 p-4 rounded-md border border-green-100"> | |
| <p class="text-sm font-medium text-gray-500">Total miles</p> | |
| <p id="total-miles-display" class="text-2xl font-bold">--</p> | |
| </div> | |
| <div class="bg-yellow-50 p-4 rounded-md border border-yellow-100"> | |
| <p class="text-sm font-medium text-gray-500">Longest run</p> | |
| <p id="longest-run-display" class="text-2xl font-bold">--</p> | |
| </div> | |
| <div class="bg-purple-50 p-4 rounded-md border border-purple-100"> | |
| <p class="text-sm font-medium text-gray-500">Quality days/week</p> | |
| <p id="quality-days-display" class="text-2xl font-bold">--</p> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex items-center"> | |
| <div class="flex-1"> | |
| <div class="h-2 w-full bg-gray-200 rounded-full overflow-hidden"> | |
| <div class="progress-bar h-full rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div id="plan-progress-display" class="ml-4 text-sm font-medium text-primary">Week 0/0</div> | |
| </div> | |
| </div> | |
| <!-- Calendar View --> | |
| <div id="calendar-view" class="hidden bg-white rounded-lg shadow p-6 mb-8"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <div id="calendar-month-year" class="text-xl font-bold text-gray-800"></div> | |
| <div class="flex space-x-2"> | |
| <button id="prev-month" class="p-2 rounded-full hover:bg-gray-100"> | |
| <i class="fas fa-chevron-left text-gray-600"></i> | |
| </button> | |
| <button id="next-month" class="p-2 rounded-full hover:bg-gray-100"> | |
| <i class="fas fa-chevron-right text-gray-600"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-7 gap-1 mb-1"> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Sun</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Mon</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Tue</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Wed</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Thu</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Fri</div> | |
| <div class="text-center text-sm font-medium py-2 text-gray-500">Sat</div> | |
| </div> | |
| <div id="calendar-grid" class="grid grid-cols-7 gap-1"> | |
| <!-- Calendar days will be inserted here by JS --> | |
| </div> | |
| </div> | |
| <!-- Weekly tabs --> | |
| <div id="weekly-tabs-container" class="bg-white rounded-lg shadow overflow-hidden"> | |
| <div class="border-b border-gray-200"> | |
| <nav id="weekly-tabs" class="-mb-px flex overflow-x-auto" aria-label="Tabs"> | |
| <!-- Week tabs will be inserted here by JS --> | |
| </nav> | |
| </div> | |
| <div id="weekly-workouts-container" class="grid grid-cols-1 md:grid-cols-3 gap-6 p-6"> | |
| <!-- Workout cards will be inserted here by JS --> | |
| </div> | |
| <div class="px-6 py-4 bg-gray-50 border-t border-gray-200 flex items-center"> | |
| <div class="flex-1"> | |
| <p class="text-sm font-medium text-gray-700">Total miles this week: <span id="current-week-total-miles">0</span></p> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="prev-week" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"> | |
| <i class="fas fa-arrow-left mr-2"></i> Previous week | |
| </button> | |
| <button id="next-week" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-blue-700"> | |
| Next week <i class="fas fa-arrow-right ml-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Garmin Integration Section --> | |
| <div class="py-12 bg-gradient-to-r from-[#005e8c] to-primary"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="lg:grid lg:grid-cols-2 lg:gap-16 items-center"> | |
| <div class="mb-12 lg:mb-0"> | |
| <h2 class="text-3xl font-extrabold tracking-tight text-white sm:text-4xl"> | |
| Seamless Garmin Integration | |
| </h2> | |
| <p class="mt-4 text-lg leading-6 text-blue-100"> | |
| Sync your training schedule directly to your Garmin device with just one click. Your workouts will appear in your Garmin Calendar with all the details. | |
| </p> | |
| <div class="mt-8"> | |
| <div class="flex items-center mb-4"> | |
| <div class="flex-shrink-0"> | |
| <div class="flex items-center justify-center h-8 w-8 rounded-full bg-blue-500"> | |
| <i class="fas fa-sync-alt text-white"></i> | |
| </div> | |
| </div> | |
| <div class="ml-4"> | |
| <h5 class="text-lg font-medium text-white">Real-time Sync</h5> | |
| <p class="mt-1 text-blue-100">Workouts appear on your watch immediately after syncing</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center mb-4"> | |
| <div class="flex-shrink-0"> | |
| <div class="flex items-center justify-center h-8 w-8 rounded-full bg-blue-500"> | |
| <i class="fas fa-bell text-white"></i> | |
| </div> | |
| </div> | |
| <div class="ml-4"> | |
| <h5 class="text-lg font-medium text-white">Workout Notifications</h5> | |
| <p class="mt-1 text-blue-100">Get reminders on your device when it's time to train</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0"> | |
| <div class="flex items-center justify-center h-8 w-8 rounded-full bg-blue-500"> | |
| <i class="fas fa-chart-line text-white"></i> | |
| </div> | |
| </div> | |
| <div class="ml-4"> | |
| <h5 class="text-lg font-medium text-white">Automatic Tracking</h5> | |
| <p class="mt-1 text-blue-100">Runs automatically recorded in Garmin Connect with metrics</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-10"> | |
| <button id="connect-garmin-badge" class="garmin-badge pulse inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-md text-white bg-opacity-90 hover:scale-105 transition"> | |
| <i class="fab fa-garmin text-2xl mr-3"></i> | |
| <span>Connect Your Garmin Account</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="relative"> | |
| <div class="relative w-full rounded-xl shadow-xl overflow-hidden"> | |
| <div class="relative h-96 bg-gray-800 rounded-xl flex items-center justify-center"> | |
| <div class="absolute inset-0 opacity-20"> | |
| <div class="grid grid-cols-3 grid-rows-3 gap-4 h-full p-4"> | |
| <div class="bg-gradient-to-br from-primary to-blue-900 rounded-lg transform rotate-3"></div> | |
| <div class="bg-gradient-to-br from-green-500 to-emerald-900 rounded-lg transform -rotate-2"></div> | |
| <div class="bg-gradient-to-br from-blue-500 to-blue-900 rounded-lg transform rotate-1"></div> | |
| <div class="bg-gradient-to-br from-primary to-blue-900 rounded-lg transform -rotate-3"></div> | |
| <div class="bg-gradient-to-br from-green-500 to-emerald-900 rounded-lg transform rotate-2"></div> | |
| <div class="bg-gradient-to-br from-primary to-blue-900 rounded-lg transform -rotate-1"></div> | |
| <div class="bg-gradient-to-br from-green-500 to-emerald-900 rounded-lg transform rotate-1"></div> | |
| <div class="bg-gradient-to-br from-blue-500 to-blue-900 rounded-lg transform -rotate-3"></div> | |
| <div class="bg-gradient-to-br from-primary to-blue-900 rounded-lg transform rotate-2"></div> | |
| </div> | |
| </div> | |
| <div class="relative bg-gray-900 w-52 h-80 rounded-3xl border-8 border-gray-800 shadow-2xl overflow-hidden flex flex-col"> | |
| <div class="h-8 bg-black flex justify-center items-center"> | |
| <div class="w-10 h-1 rounded bg-gray-700"></div> | |
| </div> | |
| <div class="flex-1 bg-gray-900 p-3"> | |
| <div class="text-white text-center font-semibold mb-2">TODAY'S RUN</div> | |
| <div class="bg-gray-800 rounded-lg p-3"> | |
| <div class="text-center text-primary font-medium">Long Run</div> | |
| <div class="flex justify-between items-center mt-2"> | |
| <div> | |
| <div class="text-xs text-gray-400">Distance</div> | |
| <div class="text-white">16 miles</div> | |
| </div> | |
| <div> | |
| <div class="text-xs text-gray-400">Target Pace</div> | |
| <div class="text-white">8:45-9:30</div> | |
| </div> | |
| </div> | |
| <div class="mt-3 flex justify-center"> | |
| <button class="bg-primary rounded-full px-3 py-1 text-white text-xs">START</button> | |
| </div> | |
| </div> | |
| <div class="mt-4 text-xs text-gray-500 text-center">Synced from RunSync</div> | |
| </div> | |
| <div class="h-8 bg-black flex items-center justify-center"> | |
| <i class="fas fa-circle text-green-500 animate-pulse"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Progress Tracking --> | |
| <div class="py-12 bg-white"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="lg:text-center"> | |
| <h2 class="text-base text-primary font-semibold tracking-wide uppercase">Track Your Progress</h2> | |
| <p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl"> | |
| Weekly Performance Metrics | |
| </p> | |
| <p class="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto"> | |
| See how your fitness improves over the training period with data synced from your Garmin device. | |
| </p> | |
| </div> | |
| <div class="mt-10"> | |
| <div class="bg-gray-50 rounded-xl shadow-md p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <div> | |
| <h3 class="text-lg font-medium text-gray-900">Training Load & Fitness</h3> | |
| <p class="text-sm text-gray-500">Based on your Garmin activity data</p> | |
| </div> | |
| <div class="relative"> | |
| <select class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm rounded-md bg-white"> | |
| <option>Last 4 weeks</option> | |
| <option>Last 8 weeks</option> | |
| <option>Full plan</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Graph Container --> | |
| <div class="h-80 rounded-lg bg-white p-4 grid grid-cols-12 items-end"> | |
| <div class="col-span-1 flex flex-col justify-between h-full text-xs text-gray-500"> | |
| <div>100</div> | |
| <div>80</div> | |
| <div>60</div> | |
| <div>40</div> | |
| <div>20</div> | |
| <div>0</div> | |
| </div> | |
| <div class="col-span-11 grid grid-cols-7 gap-4 items-end h-full"> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 30%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 70%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W1</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 40%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 65%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W2</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 55%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 70%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W3</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 50%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 75%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W4</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 65%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 85%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W5</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 60%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 80%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W6</div> | |
| </div> | |
| <div class="flex flex-col items-center justify-end"> | |
| <div class="w-10 bg-blue-200 rounded-t-md" style="height: 70%"></div> | |
| <div class="w-10 bg-primary rounded-t-md" style="height: 90%"></div> | |
| <div class="text-xs text-gray-500 mt-2">W7</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modals (for simulating login/messages) --> | |
| <div id="auth-modal" class="modal"> | |
| <div class="bg-white p-8 rounded-lg shadow-xl max-w-sm w-full text-center"> | |
| <h3 class="text-2xl font-bold text-gray-900 mb-6">Login / Register</h3> | |
| <div class="space-y-4"> | |
| <input type="email" id="auth-email" placeholder="Email" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary"> | |
| <input type="password" id="auth-password" placeholder="Password" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary"> | |
| <button id="auth-login-btn" class="w-full px-4 py-3 bg-primary text-white font-semibold rounded-lg hover:bg-blue-700 transition">Login</button> | |
| <button id="auth-register-btn" class="w-full px-4 py-3 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition">Register</button> | |
| <button id="auth-close-btn" class="w-full px-4 py-3 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="message-modal" class="modal"> | |
| <div class="bg-white p-8 rounded-lg shadow-xl max-w-sm w-full text-center"> | |
| <h3 id="message-title" class="text-xl font-bold text-gray-900 mb-4"></h3> | |
| <p id="message-text" class="text-gray-700 mb-6"></p> | |
| <button id="message-close-btn" class="px-6 py-3 bg-primary text-white font-semibold rounded-lg hover:bg-blue-700 transition">OK</button> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const mobileMenuButton = document.getElementById('mobile-menu-button'); | |
| const mobileMenu = document.getElementById('mobile-menu'); | |
| const generatePlanButton = document.getElementById('generate-plan'); | |
| const planFormContainer = document.querySelector('#plan-creator .mt-10'); // Get the div containing the form | |
| const planDisplaySection = document.getElementById('plan-display'); | |
| const calendarView = document.getElementById('calendar-view'); | |
| const calendarToggleButton = document.getElementById('calendar-toggle'); | |
| const mileageSlider = document.getElementById('mileage'); | |
| const mileageDisplay = document.getElementById('mileage-display'); | |
| const mileageNote = document.getElementById('mileage-note'); | |
| const raceDistanceSelect = document.getElementById('race-distance'); | |
| const weeksInput = document.getElementById('weeks'); | |
| const durationStatus = document.getElementById('duration-status'); | |
| const weeklyTabsContainer = document.getElementById('weekly-tabs'); | |
| const weeklyWorkoutsContainer = document.getElementById('weekly-workouts-container'); | |
| const currentWeekTotalMiles = document.getElementById('current-week-total-miles'); | |
| const prevWeekButton = document.getElementById('prev-week'); | |
| const nextWeekButton = document.getElementById('next-week'); | |
| const calendarGrid = document.getElementById('calendar-grid'); | |
| const calendarMonthYear = document.getElementById('calendar-month-year'); | |
| const prevMonthButton = document.getElementById('prev-month'); | |
| const nextMonthButton = document.getElementById('next-month'); | |
| const connectGarminHeroButton = document.getElementById('connect-garmin-hero'); | |
| const connectGarminBadgeButton = document.getElementById('connect-garmin-badge'); | |
| const syncToGarminButton = document.getElementById('sync-to-garmin-button'); | |
| // Auth UI elements | |
| const loginButton = document.getElementById('login-button'); | |
| const logoutButton = document.getElementById('logout-button'); | |
| const authModal = document.getElementById('auth-modal'); | |
| const authEmail = document.getElementById('auth-email'); | |
| const authPassword = document.getElementById('auth-password'); | |
| const authLoginBtn = document.getElementById('auth-login-btn'); | |
| const authRegisterBtn = document.getElementById('auth-register-btn'); | |
| const authCloseBtn = document.getElementById('auth-close-btn'); | |
| // Message modal elements | |
| const messageModal = document.getElementById('message-modal'); | |
| const messageTitle = document.getElementById('message-title'); | |
| const messageText = document.getElementById('message-text'); | |
| const messageCloseBtn = document.getElementById('message-close-btn'); | |
| let currentPlan = null; | |
| let currentWeekIndex = 0; // 0-indexed week | |
| // Mock Garmin state (for client-side demo) | |
| let isGarminConnected = false; | |
| // --- Utility Functions --- | |
| // Show custom message modal | |
| function showMessage(title, text) { | |
| messageTitle.textContent = title; | |
| messageText.textContent = text; | |
| messageModal.style.display = 'flex'; | |
| } | |
| // Hide custom message modal | |
| messageCloseBtn.addEventListener('click', () => { | |
| messageModal.style.display = 'none'; | |
| }); | |
| // --- Authentication Logic --- | |
| loginButton.addEventListener('click', () => { | |
| authModal.style.display = 'flex'; | |
| }); | |
| authCloseBtn.addEventListener('click', () => { | |
| authModal.style.display = 'none'; | |
| authEmail.value = ''; | |
| authPassword.value = ''; | |
| }); | |
| authLoginBtn.addEventListener('click', async () => { | |
| const email = authEmail.value; | |
| const password = authPassword.value; | |
| // Check if window.auth is available before using it | |
| if (window.auth && email && password) { | |
| try { | |
| const { signInWithEmailAndPassword } = await import("https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"); | |
| await signInWithEmailAndPassword(window.auth, email, password); | |
| showMessage('Success', 'Logged in successfully!'); | |
| authModal.style.display = 'none'; | |
| } catch (error) { | |
| showMessage('Login Failed', error.message); | |
| } | |
| } else { | |
| showMessage('Login Error', 'Please enter email and password. Firebase not initialized or fields are empty.'); | |
| } | |
| }); | |
| authRegisterBtn.addEventListener('click', async () => { | |
| const email = authEmail.value; | |
| const password = authPassword.value; | |
| // Check if window.auth is available before using it | |
| if (window.auth && email && password) { | |
| try { | |
| const { createUserWithEmailAndPassword } = await import("https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"); | |
| await createUserWithEmailAndPassword(window.auth, email, password); | |
| showMessage('Success', 'Registered and logged in successfully!'); | |
| authModal.style.display = 'none'; | |
| } catch (error) { | |
| showMessage('Registration Failed', error.message); | |
| } | |
| } else { | |
| showMessage('Registration Error', 'Please enter email and password. Firebase not initialized or fields are empty.'); | |
| } | |
| }); | |
| logoutButton.addEventListener('click', async () => { | |
| if (window.auth) { | |
| try { | |
| const { signOut } = await import("https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"); | |
| await signOut(window.auth); | |
| showMessage('Logged Out', 'You have been logged out.'); | |
| // Clear any displayed plan or user-specific data | |
| planFormContainer.classList.remove('hidden'); | |
| planDisplaySection.classList.add('hidden'); | |
| const userNameDisplay = document.getElementById('user-name-display'); | |
| const mobileUserNameDisplay = document.getElementById('mobile-user-name-display'); | |
| if (userNameDisplay) userNameDisplay.textContent = `Guest: ${window.userId ? window.userId.substring(0, 8) : 'unknown'}...`; | |
| if (mobileUserNameDisplay) mobileUserNameDisplay.textContent = `Guest: ${window.userId ? window.userId.substring(0, 8) : 'unknown'}...`; | |
| } catch (error) { | |
| showMessage('Logout Failed', error.message); | |
| } | |
| } | |
| }); | |
| // --- UI Updates based on Form Inputs --- | |
| mobileMenuButton.addEventListener('click', () => { | |
| mobileMenu.classList.toggle('hidden'); | |
| }); | |
| // Initial update for mileage display | |
| mileageDisplay.textContent = `${mileageSlider.value} miles`; | |
| updateMileageNote(mileageSlider.value); | |
| updateDurationStatus(weeksInput.value, raceDistanceSelect.value); | |
| // Update mileage display on slider change | |
| mileageSlider.addEventListener('input', () => { | |
| mileageDisplay.textContent = `${mileageSlider.value} ${document.querySelector('input[name="units"]:checked').value}`; | |
| updateMileageNote(mileageSlider.value); | |
| }); | |
| // Update duration status on weeks or race distance change | |
| weeksInput.addEventListener('input', () => { | |
| updateDurationStatus(weeksInput.value, raceDistanceSelect.value); | |
| }); | |
| raceDistanceSelect.addEventListener('change', () => { | |
| updateDurationStatus(weeksInput.value, raceDistanceSelect.value); | |
| }); | |
| document.querySelectorAll('input[name="units"]').forEach(radio => { | |
| radio.addEventListener('change', () => { | |
| mileageDisplay.textContent = `${mileageSlider.value} ${document.querySelector('input[name="units"]:checked').value}`; | |
| // Re-run plan generation or update to reflect unit changes if plan exists | |
| if (currentPlan) { | |
| displayGeneratedPlan(currentPlan); // Redraw with new units | |
| } | |
| }); | |
| }); | |
| function updateMileageNote(mileage) { | |
| let note = ""; | |
| const distance = raceDistanceSelect.value; | |
| if (distance === "26.2") { // Marathon | |
| if (mileage < 30) note = "Consider increasing mileage for marathon"; | |
| else if (mileage >= 30 && mileage <= 50) note = "Optimal for marathon training"; | |
| else note = "High mileage, ensure adequate recovery"; | |
| } else if (distance === "13.1") { // Half Marathon | |
| if (mileage < 20) note = "Might be low for half marathon"; | |
| else if (mileage >= 20 && mileage <= 40) note = "Good for half marathon"; | |
| else note = "Higher end for half marathon, strong base needed"; | |
| } else { // 5K/10K | |
| if (mileage < 15) note = "Good for building base"; | |
| else if (mileage >= 15 && mileage <= 30) note = "Optimal for 5K/10K"; | |
| else note = "High mileage for shorter distances, focus on quality"; | |
| } | |
| mileageNote.textContent = note; | |
| } | |
| function updateDurationStatus(weeks, distance) { | |
| let status = "Optimal"; | |
| if (distance === "26.2") { // Marathon | |
| if (weeks < 16) status = "Short for marathon, good base needed"; | |
| else if (weeks > 20) status = "Long, consider shorter phases"; | |
| } else if (distance === "13.1") { // Half Marathon | |
| if (weeks < 10) status = "Short for half marathon"; | |
| else if (weeks > 14) status = "Longer than typical for half marathon"; | |
| } else { // 5K/10K | |
| if (weeks < 8) status = "Short for 5K/10K"; | |
| else if (weeks > 12) status = "Longer than typical for 5K/10K"; | |
| } | |
| durationStatus.textContent = status; | |
| } | |
| // --- Plan Generation & Display Logic --- | |
| generatePlanButton.addEventListener('click', async () => { | |
| const planDetails = { | |
| raceDistance: raceDistanceSelect.value, | |
| weeks: parseInt(weeksInput.value), | |
| units: document.querySelector('input[name="units"]:checked').value, | |
| fitnessLevel: document.querySelector('input[name="fitness"]:checked').value, | |
| weeklyMileageGoal: parseInt(mileageSlider.value), | |
| goalPace: document.querySelector('input[name="pace"]:checked').value | |
| }; | |
| // Simulate plan generation (replace with actual backend call) | |
| currentPlan = generateMockPlan(planDetails); | |
| // Save the generated plan to Firestore | |
| if (window.saveUserData) { | |
| await window.saveUserData({ currentPlan: currentPlan }); | |
| } | |
| displayGeneratedPlan(currentPlan); | |
| planFormContainer.classList.add('hidden'); | |
| planDisplaySection.classList.remove('hidden'); | |
| // Ensure initial week is displayed | |
| currentWeekIndex = 0; | |
| displayWeek(currentWeekIndex); | |
| }); | |
| function generateMockPlan(details) { | |
| const plan = { | |
| ...details, | |
| weeksData: [] | |
| }; | |
| const totalMiles = details.weeklyMileageGoal * details.weeks; | |
| const longestRunBase = details.raceDistance === "26.2" ? 20 : (details.raceDistance === "13.1" ? 10 : 6); | |
| let currentMileage = details.weeklyMileageGoal * 0.7; // Start a bit lower | |
| for (let i = 0; i < details.weeks; i++) { | |
| const weekNum = i + 1; | |
| const week = { | |
| weekNum: weekNum, | |
| totalMiles: 0, | |
| workouts: [] | |
| }; | |
| const easyRunBase = currentMileage / 5; | |
| const qualityRunBase = currentMileage / 3; | |
| const longRunBase = currentMileage / 2.5; | |
| // Example workouts for a week (simplistic) | |
| week.workouts.push({ day: 'Monday', type: 'Rest Day', distance: 0, unit: details.units, pace: 'N/A', description: 'Rest or light stretching' }); | |
| week.workouts.push({ day: 'Tuesday', type: 'Easy Run', distance: Math.round(easyRunBase * 10) / 10, unit: details.units, pace: details.goalPace, description: 'Easy pace for recovery' }); | |
| week.workouts.push({ day: 'Wednesday', type: 'Tempo Run', distance: Math.round(qualityRunBase * 0.8 * 10) / 10, unit: details.units, pace: details.goalPace, description: `Tempo run with warm-up/cool-down` }); | |
| week.workouts.push({ day: 'Thursday', type: 'Easy Run', distance: Math.round(easyRunBase * 0.7 * 10) / 10, unit: details.units, pace: details.goalPace, description: 'Easy pace' }); | |
| week.workouts.push({ day: 'Friday', type: 'Rest Day', distance: 0, unit: details.units, pace: 'N/A', description: 'Rest or cross-train' }); | |
| week.workouts.push({ day: 'Saturday', type: 'Speed Work', distance: Math.round(qualityRunBase * 0.7 * 10) / 10, unit: details.units, pace: details.goalPace.split('-')[0], description: 'Intervals or hill repeats' }); | |
| week.workouts.push({ day: 'Sunday', type: 'Long Run', distance: Math.round(longRunBase * 10) / 10, unit: details.units, pace: details.goalPace, description: 'Steady long run' }); | |
| week.totalMiles = week.workouts.reduce((sum, wo) => sum + wo.distance, 0); | |
| currentMileage = Math.min(details.weeklyMileageGoal * (1 + i * 0.05), details.weeklyMileageGoal); // Gradually increase | |
| plan.weeksData.push(week); | |
| } | |
| // Calculate longest run and total miles for the plan overview | |
| plan.longestRun = Math.max(...plan.weeksData.flatMap(week => week.workouts.filter(wo => wo.type === 'Long Run').map(wo => wo.distance))); | |
| plan.totalPlanMiles = plan.weeksData.reduce((sum, week) => sum + week.totalMiles, 0); | |
| return plan; | |
| } | |
| function displayGeneratedPlan(plan) { | |
| currentPlan = plan; // Store the plan globally | |
| // Update plan overview | |
| document.getElementById('plan-title').textContent = `Your ${plan.raceDistance === "26.2" ? "Marathon" : plan.raceDistance + "K/Half"} Training Plan`; | |
| document.getElementById('plan-summary').textContent = `${plan.weeks} weeks | Goal: ${plan.goalPace} min/${plan.units} | Avg. ${plan.weeklyMileageGoal} ${plan.units}/week`; | |
| document.getElementById('total-weeks-display').textContent = plan.weeks; | |
| document.getElementById('total-miles-display').textContent = `${Math.round(plan.totalPlanMiles)}`; | |
| document.getElementById('longest-run-display').textContent = `${plan.longestRun} ${plan.units}`; | |
| document.getElementById('quality-days-display').textContent = '2'; // Based on initial description | |
| // Populate weekly tabs | |
| weeklyTabsContainer.innerHTML = ''; | |
| for (let i = 0; i < plan.weeks; i++) { | |
| const tab = document.createElement('a'); | |
| tab.href = `#`; // No actual link, just for styling | |
| tab.classList.add('whitespace-nowrap', 'py-4', 'px-4', 'border-b-2', 'border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300', 'font-medium', 'text-sm'); | |
| tab.textContent = `Week ${i + 1}`; | |
| tab.dataset.weekIndex = i; | |
| tab.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| currentWeekIndex = i; | |
| displayWeek(currentWeekIndex); | |
| }); | |
| weeklyTabsContainer.appendChild(tab); | |
| } | |
| // Initial display of the first week | |
| displayWeek(currentWeekIndex); | |
| } | |
| function displayWeek(weekIndex) { | |
| if (!currentPlan || !currentPlan.weeksData || weekIndex < 0 || weekIndex >= currentPlan.weeksData.length) { | |
| console.error("Invalid week index or plan data missing."); | |
| return; | |
| } | |
| const weekData = currentPlan.weeksData[weekIndex]; | |
| // Update weekly tabs active state | |
| Array.from(weeklyTabsContainer.children).forEach((tab, index) => { | |
| if (index === weekIndex) { | |
| tab.classList.add('border-primary', 'text-primary'); | |
| tab.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); | |
| } else { | |
| tab.classList.remove('border-primary', 'text-primary'); | |
| tab.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); | |
| } | |
| }); | |
| // Update progress bar | |
| const progressBar = planDisplaySection.querySelector('.progress-bar'); | |
| const progressPercentage = ((weekIndex + 1) / currentPlan.weeks) * 100; | |
| progressBar.style.width = `${progressPercentage}%`; | |
| document.getElementById('plan-progress-display').textContent = `Week ${weekIndex + 1}/${currentPlan.weeks}`; | |
| // Populate workout cards for the current week | |
| weeklyWorkoutsContainer.innerHTML = ''; | |
| weekData.workouts.forEach((workout, dayIndex) => { | |
| const workoutCard = document.createElement('div'); | |
| workoutCard.classList.add('workout-card', 'bg-white', 'rounded-lg', 'border', 'border-gray-200', 'p-4'); | |
| let tagColor = 'blue'; | |
| let tagText = 'Run'; | |
| let icon = 'fas fa-shoe-prints'; | |
| if (workout.type.includes('Rest')) { | |
| tagColor = 'yellow'; | |
| tagText = 'Rest Day'; | |
| icon = 'fas fa-couch'; | |
| } else if (workout.type.includes('Tempo') || workout.type.includes('Speed')) { | |
| tagColor = 'purple'; | |
| tagText = workout.type; | |
| icon = 'fas fa-tachometer-alt'; | |
| } else if (workout.type.includes('Long')) { | |
| tagColor = 'pink'; | |
| tagText = workout.type; | |
| icon = 'fas fa-running'; | |
| } | |
| workoutCard.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${tagColor}-100 text-${tagColor}-800"> | |
| ${workout.day} | |
| </span> | |
| <h4 class="mt-1 text-lg font-medium text-gray-900">${workout.type}</h4> | |
| </div> | |
| ${workout.distance > 0 ? ` | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> | |
| <i class="${icon} mr-1"></i> ${workout.distance} ${workout.unit} | |
| </span>` : ''} | |
| </div> | |
| <p class="mt-2 text-gray-600">${workout.description}</p> | |
| <div class="mt-4 flex justify-between"> | |
| <button type="button" class="inline-flex items-center px-3 py-1 border border-gray-300 rounded-md text-xs text-gray-700 bg-white hover:bg-gray-50" onclick="showMessage('Schedule Workout', 'Scheduling functionality needs to be implemented. This would typically involve Google Calendar API or an internal calendar.')"> | |
| <i class="far fa-clock mr-1"></i> Schedule | |
| </button> | |
| ${workout.distance > 0 ? ` | |
| <button type="button" class="send-to-garmin-btn inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded-md text-white bg-primary hover:bg-blue-700" data-workout-day="${dayIndex}" data-week-index="${weekIndex}"> | |
| <i class="fab fa-garmin mr-1"></i> Send to Garmin | |
| </button>` : ` | |
| <button type="button" class="w-full inline-flex justify-center items-center px-3 py-1 border border-transparent text-xs font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200" onclick="showMessage('Cross-Training', 'Adding cross-training would allow you to log non-running activities.')"> | |
| <i class="fas fa-dumbbell mr-1"></i> Add Cross-Training | |
| </button>`} | |
| </div> | |
| `; | |
| weeklyWorkoutsContainer.appendChild(workoutCard); | |
| }); | |
| // Update current week total miles | |
| currentWeekTotalMiles.textContent = Math.round(weekData.totalMiles); | |
| // Update calendar if visible | |
| if (!calendarView.classList.contains('hidden')) { | |
| renderCalendar(new Date()); // Render current month initially, later this could be tied to plan dates | |
| } | |
| } | |
| // Calendar Navigation | |
| calendarToggleButton.addEventListener('click', () => { | |
| calendarView.classList.toggle('hidden'); | |
| document.getElementById('weekly-tabs-container').classList.toggle('hidden'); | |
| if (!calendarView.classList.contains('hidden')) { | |
| renderCalendar(new Date()); // Render current month when calendar view is shown | |
| } | |
| }); | |
| let currentCalendarDate = new Date(); // Tracks the currently displayed month in calendar | |
| function renderCalendar(date) { | |
| calendarGrid.innerHTML = ''; | |
| const month = date.getMonth(); | |
| const year = date.getFullYear(); | |
| calendarMonthYear.textContent = new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' }); | |
| const firstDayOfMonth = new Date(year, month, 1).getDay(); // 0 for Sunday, 1 for Monday | |
| const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
| // Fill leading empty days | |
| for (let i = 0; i < firstDayOfMonth; i++) { | |
| const emptyDay = document.createElement('div'); | |
| emptyDay.classList.add('calendar-day', 'text-center', 'py-2', 'rounded-lg', 'text-gray-400'); | |
| // Calculate previous month's day | |
| const prevMonthLastDay = new Date(year, month, 0).getDate(); | |
| emptyDay.textContent = prevMonthLastDay - (firstDayOfMonth - 1 - i); | |
| calendarGrid.appendChild(emptyDay); | |
| } | |
| // Fill days of the month | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const dayElement = document.createElement('div'); | |
| dayElement.classList.add('calendar-day', 'text-center', 'py-2', 'rounded-lg', 'cursor-pointer', 'border-2', 'border-transparent', 'hover:border-blue-200', 'hover:bg-blue-50'); | |
| dayElement.textContent = day; | |
| // Check for workouts on this day in the current plan | |
| if (currentPlan && currentPlan.weeksData) { | |
| currentPlan.weeksData.forEach(week => { | |
| week.workouts.forEach(workout => { | |
| // This is a simplified check. A robust solution would map workout dates. | |
| // For demo, we just mark days if there's any workout. | |
| // In a real app, you'd match the specific date. | |
| if (workout.distance > 0 && Math.random() > 0.5) { // Simulate some days having workouts | |
| dayElement.classList.add('has-workout', 'bg-blue-50', 'border-blue-100'); | |
| } | |
| }); | |
| }); | |
| } | |
| calendarGrid.appendChild(dayElement); | |
| } | |
| } | |
| prevMonthButton.addEventListener('click', () => { | |
| currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); | |
| renderCalendar(currentCalendarDate); | |
| }); | |
| nextMonthButton.addEventListener('click', () => { | |
| currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); | |
| renderCalendar(currentCalendarDate); | |
| }); | |
| prevWeekButton.addEventListener('click', () => { | |
| if (currentPlan && currentWeekIndex > 0) { | |
| currentWeekIndex--; | |
| displayWeek(currentWeekIndex); | |
| } | |
| }); | |
| nextWeekButton.addEventListener('click', () => { | |
| if (currentPlan && currentWeekIndex < currentPlan.weeksData.length - 1) { | |
| currentWeekIndex++; | |
| displayWeek(currentWeekIndex); | |
| } | |
| }); | |
| // --- Garmin Integration Simulation --- | |
| // General function for Garmin connect (used by hero and badge buttons) | |
| function initiateGarminConnect() { | |
| if (isGarminConnected) { | |
| showMessage('Garmin Connected', 'Your Garmin account is already connected!'); | |
| return; | |
| } | |
| // Simulate OAuth redirection and success | |
| showMessage('Connecting to Garmin...', 'You would be redirected to Garmin Connect for authorization. This requires backend integration and Garmin Developer API access.'); | |
| setTimeout(() => { | |
| // After a delay, simulate successful connection | |
| isGarminConnected = true; | |
| showMessage('Garmin Connected!', 'Successfully linked your Garmin account. Now you can sync workouts!'); | |
| // Update UI to reflect connection status if you had a visual indicator | |
| }, 3000); | |
| } | |
| connectGarminHeroButton.addEventListener('click', initiateGarminConnect); | |
| connectGarminBadgeButton.addEventListener('click', initiateGarminConnect); | |
| // "Send to Garmin" button within workout cards | |
| document.body.addEventListener('click', (event) => { | |
| if (event.target.matches('.send-to-garmin-btn')) { | |
| if (!isGarminConnected) { | |
| showMessage('Garmin Not Connected', 'Please connect your Garmin account first to sync workouts.'); | |
| return; | |
| } | |
| const weekIdx = parseInt(event.target.dataset.weekIndex); | |
| const dayIdx = parseInt(event.target.dataset.workoutDay); | |
| const workout = currentPlan.weeksData[weekIdx].workouts[dayIdx]; | |
| showMessage('Syncing Workout to Garmin...', `Attempting to send "${workout.type} - ${workout.distance} ${workout.unit}" to Garmin. This requires backend integration.`); | |
| // Simulate API call to backend for sending workout | |
| setTimeout(() => { | |
| showMessage('Workout Synced!', `"${workout.type} - ${workout.distance} ${workout.unit}" should now appear in your Garmin Connect calendar.`); | |
| }, 2000); | |
| } | |
| }); | |
| // "Sync to Garmin" button on plan display section (for overall plan or pulling data) | |
| syncToGarminButton.addEventListener('click', () => { | |
| if (!isGarminConnected) { | |
| showMessage('Garmin Not Connected', 'Please connect your Garmin account first to sync data.'); | |
| return; | |
| } | |
| showMessage('Syncing All Data with Garmin...', 'This would involve pushing your entire plan to Garmin and pulling your recent activities. This requires extensive backend integration with Garmin Connect APIs (Activity Upload/Download, Training Plan Upload).'); | |
| // Simulate fetching progress data from Garmin | |
| setTimeout(() => { | |
| // Update mock streak data | |
| document.getElementById('streak-display').textContent = '21-day streak'; | |
| document.getElementById('best-streak-display').textContent = '35 days'; | |
| showMessage('Sync Complete!', 'Your plan and recent activities have been synced with Garmin (simulated).'); | |
| }, 3000); | |
| }); | |
| // Initialize display on page load | |
| // The onAuthStateChanged listener in the module script will call loadUserData | |
| // once auth is ready, so no need to call it again here. | |
| }); | |
| </script> | |
| </body> | |
| </html> | |