1// Storage layer: localStorage-backed, zero-server, offline-first 2const Storage = { 3 _key: 'ironlog_data', 4 _data: null, 5 6 init() { 7 const raw = localStorage.getItem(this._key); 8 this._data = raw ? JSON.parse(raw) : { 9 workouts: [], 10 streak: 0, 11 lastWorkoutDate: null, 12 stats: { totalWorkouts: 0, totalSets: 0, totalVolume: 0 }, 13 settings: { unit: 'kg', restTimer: true } 14 }; 15 return this; 16 }, 17 18 _save() { 19 localStorage.setItem(this._key, JSON.stringify(this._data)); 20 }, 21 22 // Save a completed workout 23 saveWorkout(workout) { 24 const today = new Date().toISOString().split('T')[0]; 25 workout.date = today; 26 workout.time = new Date().toISOString(); 27 28 // Calculate volume 29 let volume = 0, sets = 0; 30 workout.exercises.forEach(ex => { 31 ex.sets_data.forEach(s => { 32 if (s.weight && s.reps) volume += s.weight * s.reps; 33 sets++; 34 }); 35 }); 36 workout.volume = volume; 37 workout.sets = sets; 38 39 this._data.workouts.unshift(workout); 40 41 // Update streak 42 if (this._data.lastWorkoutDate !== today) { 43 const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; 44 this._data.streak = (this._data.lastWorkoutDate === yesterday) 45 ? this._data.streak + 1 : 1; 46 this._data.lastWorkoutDate = today; 47 } 48 49 // Update stats 50 this._data.stats.totalWorkouts++; 51 this._data.stats.totalSets += sets; 52 this._data.stats.totalVolume += volume; 53 54 this._save(); 55 return workout; 56 }, 57 58 // Get recent workouts 59 getRecentWorkouts(limit = 10) { 60 return this._data.workouts.slice(0, limit); 61 }, 62 63 // Get today's workout (if exists) 64 getTodayWorkout() { 65 const today = new Date().toISOString().split('T')[0]; 66 return this._data.workouts.find(w => w.date === today); 67 }, 68 69 // Get streak 70 getStreak() { 71 return this._data.streak; 72 }, 73 74 // Get all stats 75 getStats() { 76 return { ...this._data.stats, streak: this._data.streak }; 77 }, 78 79 // Export CSV 80 exportCSV() { 81 const rows = [['Date', 'Template', 'Exercise', 'Sets', 'Weight(kg)', 'Reps']]; 82 this._data.workouts.forEach(w => { 83 w.exercises.forEach(ex => { 84 ex.sets_data.forEach(s => { 85 rows.push([w.date, w.templateName, ex.nameCN, '', s.weight || '', s.reps || '']); 86 }); 87 }); 88 }); 89 return rows.map(r => r.join(',')).join('\n'); 90 }, 91 92 // Get specific template's history (for "last time" auto-fill) 93 getLastExerciseData(templateId, exerciseTag) { 94 const prev = this._data.workouts.find(w => 95 w.templateId === templateId && 96 w.exercises.some(e => e.tag === exerciseTag) 97 ); 98 if (!prev) return null; 99 const ex = prev.exercises.find(e => e.tag === exerciseTag); 100 return ex ? ex.sets_data : null; 101 }, 102 103 // Check if user should be prompted for sync (3+ workouts) 104 shouldPromptSync() { 105 return this._data.workouts.length >= 3 && !this._data.syncPrompted; 106 }, 107 108 markSyncPrompted() { 109 this._data.syncPrompted = true; 110 this._save(); 111 }, 112 113 // D1 sync (future) 114 async syncToCloud() { 115 // Placeholder for Cloudflare D1 sync 116 console.log('Cloud sync not yet implemented'); 117 } 118};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.