claude-coach 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +84 -0
  3. package/bin/claude-coach.js +10 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +277 -0
  6. package/dist/db/client.d.ts +4 -0
  7. package/dist/db/client.js +45 -0
  8. package/dist/db/migrate.d.ts +1 -0
  9. package/dist/db/migrate.js +14 -0
  10. package/dist/lib/config.d.ts +27 -0
  11. package/dist/lib/config.js +86 -0
  12. package/dist/lib/logging.d.ts +13 -0
  13. package/dist/lib/logging.js +28 -0
  14. package/dist/schema/training-plan.d.ts +288 -0
  15. package/dist/schema/training-plan.js +88 -0
  16. package/dist/strava/api.d.ts +5 -0
  17. package/dist/strava/api.js +63 -0
  18. package/dist/strava/oauth.d.ts +4 -0
  19. package/dist/strava/oauth.js +113 -0
  20. package/dist/strava/types.d.ts +46 -0
  21. package/dist/strava/types.js +1 -0
  22. package/dist/viewer/lib/export/erg.d.ts +26 -0
  23. package/dist/viewer/lib/export/erg.js +206 -0
  24. package/dist/viewer/lib/export/fit.d.ts +25 -0
  25. package/dist/viewer/lib/export/fit.js +307 -0
  26. package/dist/viewer/lib/export/ics.d.ts +13 -0
  27. package/dist/viewer/lib/export/ics.js +138 -0
  28. package/dist/viewer/lib/export/index.d.ts +50 -0
  29. package/dist/viewer/lib/export/index.js +229 -0
  30. package/dist/viewer/lib/export/zwo.d.ts +21 -0
  31. package/dist/viewer/lib/export/zwo.js +230 -0
  32. package/dist/viewer/lib/utils.d.ts +14 -0
  33. package/dist/viewer/lib/utils.js +118 -0
  34. package/dist/viewer/main.d.ts +5 -0
  35. package/dist/viewer/main.js +6 -0
  36. package/dist/viewer/stores/changes.d.ts +21 -0
  37. package/dist/viewer/stores/changes.js +49 -0
  38. package/dist/viewer/stores/plan.d.ts +4 -0
  39. package/dist/viewer/stores/plan.js +19 -0
  40. package/dist/viewer/stores/settings.d.ts +53 -0
  41. package/dist/viewer/stores/settings.js +207 -0
  42. package/package.json +55 -0
  43. package/templates/plan-viewer.html +70 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Training Plan Export
3
+ *
4
+ * Unified export interface for generating workout files client-side.
5
+ * Supports:
6
+ * - ZWO (Zwift workouts) - bike/run only
7
+ * - FIT (Garmin workouts) - all sports
8
+ * - MRC (ERG/MRC) - bike only, for indoor trainers
9
+ * - ICS (iCalendar) - full plan calendar events
10
+ */
11
+ import type { Workout, TrainingPlan, Sport } from "../../../schema/training-plan.js";
12
+ import type { Settings } from "../../stores/settings.js";
13
+ export type ExportFormat = "zwo" | "fit" | "mrc" | "ics";
14
+ export interface ExportResult {
15
+ success: boolean;
16
+ filename: string;
17
+ error?: string;
18
+ }
19
+ /**
20
+ * Trigger a file download in the browser
21
+ */
22
+ export declare function downloadFile(content: string | Uint8Array, filename: string, mimeType: string): void;
23
+ /**
24
+ * Sanitize filename to be safe across platforms
25
+ */
26
+ export declare function sanitizeFilename(name: string): string;
27
+ /**
28
+ * Get available export formats for a workout based on its sport
29
+ */
30
+ export declare function getAvailableFormats(sport: Sport): ExportFormat[];
31
+ /**
32
+ * Export a single workout to the specified format
33
+ */
34
+ export declare function exportWorkout(workout: Workout, format: ExportFormat, settings: Settings): Promise<ExportResult>;
35
+ /**
36
+ * Export the full training plan to iCalendar format
37
+ */
38
+ export declare function exportPlanToCalendar(plan: TrainingPlan): ExportResult;
39
+ /**
40
+ * Export all workouts in the plan to a single ZIP file
41
+ * Contains ZWO/FIT/MRC files based on the selected format
42
+ */
43
+ export declare function exportAllWorkouts(plan: TrainingPlan, format: "zwo" | "fit" | "mrc", settings: Settings): Promise<{
44
+ exported: number;
45
+ skipped: number;
46
+ errors: string[];
47
+ }>;
48
+ export { isZwoSupported } from "./zwo.js";
49
+ export { isFitSupported, isFitSdkAvailable } from "./fit.js";
50
+ export { isErgSupported } from "./erg.js";
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Training Plan Export
3
+ *
4
+ * Unified export interface for generating workout files client-side.
5
+ * Supports:
6
+ * - ZWO (Zwift workouts) - bike/run only
7
+ * - FIT (Garmin workouts) - all sports
8
+ * - MRC (ERG/MRC) - bike only, for indoor trainers
9
+ * - ICS (iCalendar) - full plan calendar events
10
+ */
11
+ import { generateZwo, isZwoSupported } from "./zwo.js";
12
+ import { generateFit, isFitSupported } from "./fit.js";
13
+ import { generateMrc, isErgSupported } from "./erg.js";
14
+ import { generateIcs } from "./ics.js";
15
+ import JSZip from "jszip";
16
+ /**
17
+ * Trigger a file download in the browser
18
+ */
19
+ export function downloadFile(content, filename, mimeType) {
20
+ const blob = content instanceof Uint8Array
21
+ ? new Blob([new Uint8Array(content)], { type: mimeType })
22
+ : new Blob([content], { type: mimeType });
23
+ const url = URL.createObjectURL(blob);
24
+ const link = document.createElement("a");
25
+ link.href = url;
26
+ link.download = filename;
27
+ document.body.appendChild(link);
28
+ link.click();
29
+ document.body.removeChild(link);
30
+ URL.revokeObjectURL(url);
31
+ }
32
+ /**
33
+ * Sanitize filename to be safe across platforms
34
+ */
35
+ export function sanitizeFilename(name) {
36
+ return name
37
+ .replace(/[<>:"/\\|?*]/g, "") // Remove invalid chars
38
+ .replace(/\s+/g, "_") // Replace spaces with underscores
39
+ .substring(0, 100); // Limit length
40
+ }
41
+ /**
42
+ * Get available export formats for a workout based on its sport
43
+ */
44
+ export function getAvailableFormats(sport) {
45
+ const formats = [];
46
+ if (isZwoSupported(sport)) {
47
+ formats.push("zwo");
48
+ }
49
+ if (isFitSupported(sport)) {
50
+ formats.push("fit");
51
+ }
52
+ if (isErgSupported(sport)) {
53
+ formats.push("mrc");
54
+ }
55
+ return formats;
56
+ }
57
+ /**
58
+ * Export a single workout to the specified format
59
+ */
60
+ export async function exportWorkout(workout, format, settings) {
61
+ const safeName = sanitizeFilename(workout.name);
62
+ try {
63
+ switch (format) {
64
+ case "zwo": {
65
+ if (!isZwoSupported(workout.sport)) {
66
+ return {
67
+ success: false,
68
+ filename: "",
69
+ error: `Zwift export only supports bike and run workouts`,
70
+ };
71
+ }
72
+ const zwoContent = generateZwo(workout, settings);
73
+ const filename = `${safeName}.zwo`;
74
+ downloadFile(zwoContent, filename, "application/xml");
75
+ return { success: true, filename };
76
+ }
77
+ case "fit": {
78
+ if (!isFitSupported(workout.sport)) {
79
+ return {
80
+ success: false,
81
+ filename: "",
82
+ error: `FIT export not supported for ${workout.sport} workouts`,
83
+ };
84
+ }
85
+ const fitContent = await generateFit(workout, settings);
86
+ const filename = `${safeName}.fit`;
87
+ downloadFile(fitContent, filename, "application/vnd.ant.fit");
88
+ return { success: true, filename };
89
+ }
90
+ case "mrc": {
91
+ if (!isErgSupported(workout.sport)) {
92
+ return {
93
+ success: false,
94
+ filename: "",
95
+ error: `MRC export only supports bike workouts`,
96
+ };
97
+ }
98
+ const mrcContent = generateMrc(workout, settings);
99
+ const filename = `${safeName}.mrc`;
100
+ downloadFile(mrcContent, filename, "text/plain");
101
+ return { success: true, filename };
102
+ }
103
+ default:
104
+ return {
105
+ success: false,
106
+ filename: "",
107
+ error: `Unknown format: ${format}`,
108
+ };
109
+ }
110
+ }
111
+ catch (error) {
112
+ return {
113
+ success: false,
114
+ filename: "",
115
+ error: error instanceof Error ? error.message : "Unknown error",
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * Export the full training plan to iCalendar format
121
+ */
122
+ export function exportPlanToCalendar(plan) {
123
+ try {
124
+ const icsContent = generateIcs(plan);
125
+ const safeName = sanitizeFilename(plan.meta.event);
126
+ const filename = `${safeName}_training_plan.ics`;
127
+ downloadFile(icsContent, filename, "text/calendar");
128
+ return { success: true, filename };
129
+ }
130
+ catch (error) {
131
+ return {
132
+ success: false,
133
+ filename: "",
134
+ error: error instanceof Error ? error.message : "Unknown error",
135
+ };
136
+ }
137
+ }
138
+ /**
139
+ * Generate workout file content without triggering a download
140
+ */
141
+ async function generateWorkoutContent(workout, format, settings) {
142
+ const safeName = sanitizeFilename(workout.name);
143
+ try {
144
+ switch (format) {
145
+ case "zwo": {
146
+ if (!isZwoSupported(workout.sport))
147
+ return null;
148
+ const content = generateZwo(workout, settings);
149
+ return { content, filename: `${safeName}.zwo` };
150
+ }
151
+ case "fit": {
152
+ if (!isFitSupported(workout.sport))
153
+ return null;
154
+ const content = await generateFit(workout, settings);
155
+ return { content, filename: `${safeName}.fit` };
156
+ }
157
+ case "mrc": {
158
+ if (!isErgSupported(workout.sport))
159
+ return null;
160
+ const content = generateMrc(workout, settings);
161
+ return { content, filename: `${safeName}.mrc` };
162
+ }
163
+ default:
164
+ return null;
165
+ }
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ /**
172
+ * Export all workouts in the plan to a single ZIP file
173
+ * Contains ZWO/FIT/MRC files based on the selected format
174
+ */
175
+ export async function exportAllWorkouts(plan, format, settings) {
176
+ const errors = [];
177
+ let exported = 0;
178
+ let skipped = 0;
179
+ const zip = new JSZip();
180
+ for (const week of plan.weeks) {
181
+ for (const day of week.days) {
182
+ for (const workout of day.workouts) {
183
+ // Skip rest days
184
+ if (workout.sport === "rest") {
185
+ skipped++;
186
+ continue;
187
+ }
188
+ // Check format support
189
+ if (format === "zwo" && !isZwoSupported(workout.sport)) {
190
+ skipped++;
191
+ continue;
192
+ }
193
+ if (format === "fit" && !isFitSupported(workout.sport)) {
194
+ skipped++;
195
+ continue;
196
+ }
197
+ if (format === "mrc" && !isErgSupported(workout.sport)) {
198
+ skipped++;
199
+ continue;
200
+ }
201
+ try {
202
+ const result = await generateWorkoutContent(workout, format, settings);
203
+ if (result) {
204
+ zip.file(result.filename, result.content);
205
+ exported++;
206
+ }
207
+ else {
208
+ skipped++;
209
+ }
210
+ }
211
+ catch (error) {
212
+ errors.push(`${workout.name}: ${error instanceof Error ? error.message : "Unknown error"}`);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ // Only create and download the ZIP if there are files to export
218
+ if (exported > 0) {
219
+ const zipContent = await zip.generateAsync({ type: "uint8array" });
220
+ const planName = sanitizeFilename(plan.meta.event);
221
+ const zipFilename = `${planName}_workouts_${format}.zip`;
222
+ downloadFile(zipContent, zipFilename, "application/zip");
223
+ }
224
+ return { exported, skipped, errors };
225
+ }
226
+ // Re-export format-specific helpers
227
+ export { isZwoSupported } from "./zwo.js";
228
+ export { isFitSupported, isFitSdkAvailable } from "./fit.js";
229
+ export { isErgSupported } from "./erg.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ZWO (Zwift Workout) Export
3
+ *
4
+ * Generates Zwift workout files in XML format.
5
+ * Only supports bike and run workouts.
6
+ *
7
+ * How to import to Zwift:
8
+ * - Desktop: Save to Documents/Zwift/Workouts/[userID]/
9
+ * - iOS/iPad: Open Zwift on desktop first (syncs to cloud), then iOS downloads it
10
+ * - Alternative: Save to iCloud at Documents/Zwift/Workouts/[userID]/
11
+ */
12
+ import type { Workout, Sport } from "../../../schema/training-plan.js";
13
+ import type { Settings } from "../../stores/settings.js";
14
+ /**
15
+ * Check if a sport is supported by Zwift export
16
+ */
17
+ export declare function isZwoSupported(sport: Sport): boolean;
18
+ /**
19
+ * Generate a complete ZWO file for a workout
20
+ */
21
+ export declare function generateZwo(workout: Workout, _settings: Settings): string;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * ZWO (Zwift Workout) Export
3
+ *
4
+ * Generates Zwift workout files in XML format.
5
+ * Only supports bike and run workouts.
6
+ *
7
+ * How to import to Zwift:
8
+ * - Desktop: Save to Documents/Zwift/Workouts/[userID]/
9
+ * - iOS/iPad: Open Zwift on desktop first (syncs to cloud), then iOS downloads it
10
+ * - Alternative: Save to iCloud at Documents/Zwift/Workouts/[userID]/
11
+ */
12
+ /**
13
+ * Check if a sport is supported by Zwift export
14
+ */
15
+ export function isZwoSupported(sport) {
16
+ return sport === "bike" || sport === "run";
17
+ }
18
+ /**
19
+ * Escape XML special characters
20
+ */
21
+ function escapeXml(str) {
22
+ return str
23
+ .replace(/&/g, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;")
27
+ .replace(/'/g, "&apos;");
28
+ }
29
+ /**
30
+ * Convert intensity percentage to Zwift decimal format
31
+ * (e.g., 75% FTP -> 0.75)
32
+ */
33
+ function intensityToDecimal(intensity) {
34
+ // Intensity is stored as percentage (e.g., 75 for 75%)
35
+ // Zwift expects decimal (0.75)
36
+ return intensity / 100;
37
+ }
38
+ /**
39
+ * Convert duration to seconds
40
+ */
41
+ function durationToSeconds(value, unit) {
42
+ switch (unit) {
43
+ case "seconds":
44
+ return value;
45
+ case "minutes":
46
+ return value * 60;
47
+ case "hours":
48
+ return value * 3600;
49
+ default:
50
+ // For distance-based durations, estimate using typical speeds
51
+ // This is a rough approximation
52
+ return value; // Return as-is if not time-based
53
+ }
54
+ }
55
+ /**
56
+ * Get workout type for ZWO (bike or run)
57
+ */
58
+ function getZwoSportType(sport) {
59
+ return sport === "run" ? "run" : "bike";
60
+ }
61
+ /**
62
+ * Generate a warmup segment
63
+ */
64
+ function generateWarmup(step) {
65
+ const duration = durationToSeconds(step.duration.value, step.duration.unit);
66
+ const startPower = intensityToDecimal(step.intensity.valueLow || step.intensity.value * 0.6);
67
+ const endPower = intensityToDecimal(step.intensity.valueHigh || step.intensity.value);
68
+ return ` <Warmup Duration="${duration}" PowerLow="${startPower.toFixed(2)}" PowerHigh="${endPower.toFixed(2)}"/>`;
69
+ }
70
+ /**
71
+ * Generate a cooldown segment
72
+ */
73
+ function generateCooldown(step) {
74
+ const duration = durationToSeconds(step.duration.value, step.duration.unit);
75
+ const startPower = intensityToDecimal(step.intensity.valueHigh || step.intensity.value);
76
+ const endPower = intensityToDecimal(step.intensity.valueLow || step.intensity.value * 0.5);
77
+ return ` <Cooldown Duration="${duration}" PowerLow="${endPower.toFixed(2)}" PowerHigh="${startPower.toFixed(2)}"/>`;
78
+ }
79
+ /**
80
+ * Generate a steady state segment
81
+ */
82
+ function generateSteadyState(step, isRamp = false) {
83
+ const duration = durationToSeconds(step.duration.value, step.duration.unit);
84
+ const power = intensityToDecimal(step.intensity.value);
85
+ let cadenceAttr = "";
86
+ if (step.cadence) {
87
+ cadenceAttr = ` Cadence="${step.cadence.low}"`;
88
+ if (step.cadence.high !== step.cadence.low) {
89
+ cadenceAttr = ` CadenceLow="${step.cadence.low}" CadenceHigh="${step.cadence.high}"`;
90
+ }
91
+ }
92
+ if (isRamp && step.intensity.valueLow !== undefined && step.intensity.valueHigh !== undefined) {
93
+ const powerLow = intensityToDecimal(step.intensity.valueLow);
94
+ const powerHigh = intensityToDecimal(step.intensity.valueHigh);
95
+ return ` <Ramp Duration="${duration}" PowerLow="${powerLow.toFixed(2)}" PowerHigh="${powerHigh.toFixed(2)}"${cadenceAttr}/>`;
96
+ }
97
+ return ` <SteadyState Duration="${duration}" Power="${power.toFixed(2)}"${cadenceAttr}/>`;
98
+ }
99
+ /**
100
+ * Generate an interval set (IntervalsT in ZWO)
101
+ */
102
+ function generateIntervalSet(intervalSet) {
103
+ // Find work and recovery steps
104
+ const workStep = intervalSet.steps.find((s) => s.type === "work");
105
+ const recoveryStep = intervalSet.steps.find((s) => s.type === "recovery" || s.type === "rest");
106
+ if (!workStep) {
107
+ // Just generate steady states if no work step found
108
+ return intervalSet.steps.map((s) => generateSteadyState(s)).join("\n");
109
+ }
110
+ const onDuration = durationToSeconds(workStep.duration.value, workStep.duration.unit);
111
+ const onPower = intensityToDecimal(workStep.intensity.value);
112
+ const offDuration = recoveryStep
113
+ ? durationToSeconds(recoveryStep.duration.value, recoveryStep.duration.unit)
114
+ : 60; // Default 60s recovery
115
+ const offPower = recoveryStep ? intensityToDecimal(recoveryStep.intensity.value) : 0.5; // Default 50% recovery
116
+ let cadenceAttr = "";
117
+ if (workStep.cadence) {
118
+ cadenceAttr = ` Cadence="${workStep.cadence.low}"`;
119
+ }
120
+ return ` <IntervalsT Repeat="${intervalSet.repeats}" OnDuration="${onDuration}" OffDuration="${offDuration}" OnPower="${onPower.toFixed(2)}" OffPower="${offPower.toFixed(2)}"${cadenceAttr}/>`;
121
+ }
122
+ /**
123
+ * Generate workout segments from structured workout
124
+ */
125
+ function generateSegmentsFromStructure(structure) {
126
+ const segments = [];
127
+ // Warmup
128
+ if (structure.warmup && structure.warmup.length > 0) {
129
+ for (const step of structure.warmup) {
130
+ if (step.type === "warmup") {
131
+ segments.push(generateWarmup(step));
132
+ }
133
+ else {
134
+ segments.push(generateSteadyState(step));
135
+ }
136
+ }
137
+ }
138
+ // Main set
139
+ for (const item of structure.main) {
140
+ if ("repeats" in item) {
141
+ // Interval set
142
+ segments.push(generateIntervalSet(item));
143
+ }
144
+ else {
145
+ // Single step
146
+ const step = item;
147
+ segments.push(generateSteadyState(step));
148
+ }
149
+ }
150
+ // Cooldown
151
+ if (structure.cooldown && structure.cooldown.length > 0) {
152
+ for (const step of structure.cooldown) {
153
+ if (step.type === "cooldown") {
154
+ segments.push(generateCooldown(step));
155
+ }
156
+ else {
157
+ segments.push(generateSteadyState(step));
158
+ }
159
+ }
160
+ }
161
+ return segments;
162
+ }
163
+ /**
164
+ * Generate a simple workout when no structure is provided
165
+ * Creates warmup -> main -> cooldown based on duration
166
+ */
167
+ function generateSimpleWorkout(workout) {
168
+ const segments = [];
169
+ const totalMinutes = workout.durationMinutes || 60;
170
+ // 10% warmup (min 5 min, max 15 min)
171
+ const warmupMinutes = Math.min(15, Math.max(5, Math.round(totalMinutes * 0.1)));
172
+ segments.push(` <Warmup Duration="${warmupMinutes * 60}" PowerLow="0.40" PowerHigh="0.65"/>`);
173
+ // Main set based on workout type
174
+ let mainPower = 0.65; // Default endurance
175
+ switch (workout.type) {
176
+ case "recovery":
177
+ mainPower = 0.55;
178
+ break;
179
+ case "endurance":
180
+ mainPower = 0.65;
181
+ break;
182
+ case "tempo":
183
+ mainPower = 0.8;
184
+ break;
185
+ case "threshold":
186
+ mainPower = 0.95;
187
+ break;
188
+ case "vo2max":
189
+ mainPower = 1.1;
190
+ break;
191
+ case "intervals":
192
+ mainPower = 0.85;
193
+ break;
194
+ case "long":
195
+ mainPower = 0.65;
196
+ break;
197
+ }
198
+ // 10% cooldown (min 5 min, max 10 min)
199
+ const cooldownMinutes = Math.min(10, Math.max(5, Math.round(totalMinutes * 0.1)));
200
+ const mainMinutes = totalMinutes - warmupMinutes - cooldownMinutes;
201
+ segments.push(` <SteadyState Duration="${mainMinutes * 60}" Power="${mainPower.toFixed(2)}"/>`);
202
+ segments.push(` <Cooldown Duration="${cooldownMinutes * 60}" PowerLow="0.40" PowerHigh="0.60"/>`);
203
+ return segments;
204
+ }
205
+ /**
206
+ * Generate a complete ZWO file for a workout
207
+ */
208
+ export function generateZwo(workout, _settings) {
209
+ if (!isZwoSupported(workout.sport)) {
210
+ throw new Error(`ZWO export not supported for ${workout.sport} workouts`);
211
+ }
212
+ const sportType = getZwoSportType(workout.sport);
213
+ const segments = workout.structure
214
+ ? generateSegmentsFromStructure(workout.structure)
215
+ : generateSimpleWorkout(workout);
216
+ // Clean up description for XML
217
+ const description = escapeXml((workout.description || "") +
218
+ (workout.humanReadable ? `\n\n${workout.humanReadable.replace(/\\n/g, "\n")}` : ""));
219
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
220
+ <workout_file>
221
+ <author>Claude Coach</author>
222
+ <name>${escapeXml(workout.name)}</name>
223
+ <description>${description}</description>
224
+ <sportType>${sportType}</sportType>
225
+ <workout>
226
+ ${segments.join("\n")}
227
+ </workout>
228
+ </workout_file>`;
229
+ return xml;
230
+ }
@@ -0,0 +1,14 @@
1
+ import type { Settings } from "../stores/settings.js";
2
+ import type { Sport } from "../../schema/training-plan.js";
3
+ export declare function formatDuration(minutes: number | undefined): string;
4
+ export declare function formatDistance(meters: number | undefined, sport: Sport, settings: Settings): string;
5
+ export declare function formatDate(dateStr: string): string;
6
+ export declare function formatEventDate(dateStr: string): string;
7
+ export declare function getZoneInfo(sport: Sport, zoneStr: string | undefined, settings: Settings): string;
8
+ export declare function getDaysToEvent(eventDate: string): number;
9
+ export declare function getOrderedDays(firstDayOfWeek: "monday" | "sunday"): string[];
10
+ export declare function getTodayISO(): string;
11
+ export declare function formatDateISO(date: Date): string;
12
+ export declare function parseDate(dateStr: string): Date;
13
+ export declare function getSportColor(sport: Sport): string;
14
+ export declare function getSportIcon(sport: Sport): string;
@@ -0,0 +1,118 @@
1
+ const METERS_PER_YARD = 0.9144;
2
+ const KM_PER_MILE = 1.60934;
3
+ export function formatDuration(minutes) {
4
+ if (!minutes)
5
+ return "";
6
+ const h = Math.floor(minutes / 60);
7
+ const m = minutes % 60;
8
+ if (h > 0) {
9
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
10
+ }
11
+ return `${m}m`;
12
+ }
13
+ export function formatDistance(meters, sport, settings) {
14
+ if (!meters)
15
+ return "";
16
+ if (sport === "swim") {
17
+ if (settings.units.swim === "yards") {
18
+ const yards = meters / METERS_PER_YARD;
19
+ return `${Math.round(yards)}yd`;
20
+ }
21
+ return `${Math.round(meters)}m`;
22
+ }
23
+ if (sport === "bike" || sport === "run") {
24
+ const unit = settings.units[sport];
25
+ if (unit === "miles") {
26
+ const miles = meters / 1000 / KM_PER_MILE;
27
+ return miles >= 10 ? `${Math.round(miles)}mi` : `${miles.toFixed(1)}mi`;
28
+ }
29
+ const km = meters / 1000;
30
+ return km >= 10 ? `${Math.round(km)}km` : `${km.toFixed(1)}km`;
31
+ }
32
+ return `${Math.round(meters)}m`;
33
+ }
34
+ export function formatDate(dateStr) {
35
+ const date = new Date(dateStr);
36
+ return date.toLocaleDateString("en-US", {
37
+ weekday: "long",
38
+ month: "short",
39
+ day: "numeric",
40
+ });
41
+ }
42
+ export function formatEventDate(dateStr) {
43
+ const date = new Date(dateStr);
44
+ return date.toLocaleDateString("en-US", {
45
+ weekday: "long",
46
+ month: "long",
47
+ day: "numeric",
48
+ year: "numeric",
49
+ });
50
+ }
51
+ export function getZoneInfo(sport, zoneStr, settings) {
52
+ if (!zoneStr)
53
+ return "";
54
+ const match = zoneStr.match(/(?:Zone\s*)?(\d)(?:\s*-\s*(\d))?/i);
55
+ if (!match)
56
+ return zoneStr;
57
+ const z1 = parseInt(match[1]);
58
+ const z2 = match[2] ? parseInt(match[2]) : z1;
59
+ let info = zoneStr;
60
+ if (sport === "run" || sport === "bike") {
61
+ const hrZones = settings[sport]?.hrZones;
62
+ if (hrZones && hrZones[z1 - 1]) {
63
+ const low = hrZones[z1 - 1].low;
64
+ const high = z2 !== z1 && hrZones[z2 - 1] ? hrZones[z2 - 1].high : hrZones[z1 - 1].high;
65
+ info += ` (${low}-${high} bpm)`;
66
+ }
67
+ }
68
+ return info;
69
+ }
70
+ export function getDaysToEvent(eventDate) {
71
+ const daysLeft = Math.ceil((new Date(eventDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
72
+ return Math.max(0, daysLeft);
73
+ }
74
+ export function getOrderedDays(firstDayOfWeek) {
75
+ if (firstDayOfWeek === "monday") {
76
+ return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
77
+ }
78
+ return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
79
+ }
80
+ export function getTodayISO() {
81
+ return formatDateISO(new Date());
82
+ }
83
+ // Format date as YYYY-MM-DD without timezone conversion
84
+ export function formatDateISO(date) {
85
+ const year = date.getFullYear();
86
+ const month = String(date.getMonth() + 1).padStart(2, "0");
87
+ const day = String(date.getDate()).padStart(2, "0");
88
+ return `${year}-${month}-${day}`;
89
+ }
90
+ // Parse ISO date string to Date object (at midnight local time)
91
+ export function parseDate(dateStr) {
92
+ const [year, month, day] = dateStr.split("-").map(Number);
93
+ return new Date(year, month - 1, day);
94
+ }
95
+ export function getSportColor(sport) {
96
+ const colors = {
97
+ swim: "var(--swim)",
98
+ bike: "var(--bike)",
99
+ run: "var(--run)",
100
+ strength: "var(--strength)",
101
+ brick: "var(--brick)",
102
+ race: "var(--race)",
103
+ rest: "var(--rest)",
104
+ };
105
+ return colors[sport] || "var(--text-muted)";
106
+ }
107
+ export function getSportIcon(sport) {
108
+ const icons = {
109
+ swim: "\u{1F3CA}",
110
+ bike: "\u{1F6B4}",
111
+ run: "\u{1F3C3}",
112
+ strength: "\u{1F4AA}",
113
+ brick: "\u{1F525}",
114
+ race: "\u{1F3C6}",
115
+ rest: "\u{1F6CC}",
116
+ };
117
+ return icons[sport] || "";
118
+ }
@@ -0,0 +1,5 @@
1
+ declare const app: {
2
+ $on?(type: string, callback: (e: any) => void): () => void;
3
+ $set?(props: Partial<Record<string, any>>): void;
4
+ } & Record<string, any>;
5
+ export default app;
@@ -0,0 +1,6 @@
1
+ import App from "./App.svelte";
2
+ import { mount } from "svelte";
3
+ const app = mount(App, {
4
+ target: document.getElementById("app"),
5
+ });
6
+ export default app;