Skip to content

Commit b946de7

Browse files
committed
Export / Import feature
1 parent 235eb80 commit b946de7

6 files changed

Lines changed: 175 additions & 105 deletions

File tree

src/components/active-workout-screen.tsx

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,10 @@ export function ActiveWorkoutScreen({ workout, motivations, onComplete, onCancel
151151
flex: 1,
152152
}}
153153
>
154-
{ordered.map((s, i) => (
154+
{ordered.map((s) => (
155155
<SetRow
156156
key={s.id}
157157
set={s}
158-
number={i + 1}
159158
onWeightChange={(w) => updateWeight(s.id, w)}
160159
onToggleDone={() => toggleDone(s.id)}
161160
/>
@@ -196,12 +195,11 @@ export function ActiveWorkoutScreen({ workout, motivations, onComplete, onCancel
196195

197196
type SetRowProps = {
198197
set: ActiveSet;
199-
number: number;
200198
onWeightChange: (w: number) => void;
201199
onToggleDone: () => void;
202200
};
203201

204-
function SetRow({ set, number, onWeightChange, onToggleDone }: SetRowProps) {
202+
function SetRow({ set, onWeightChange, onToggleDone }: SetRowProps) {
205203
const lib = findExercise(set.name);
206204
return (
207205
<div
@@ -219,37 +217,6 @@ function SetRow({ set, number, onWeightChange, onToggleDone }: SetRowProps) {
219217
}}
220218
>
221219
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
222-
<div
223-
style={{
224-
width: 28,
225-
height: 28,
226-
borderRadius: 6,
227-
background: set.done ? "#52c878" : "#0a0a0a",
228-
color: "#fff",
229-
display: "flex",
230-
alignItems: "center",
231-
justifyContent: "center",
232-
fontFamily: "var(--mono-font)",
233-
fontSize: 11,
234-
fontWeight: 700,
235-
flexShrink: 0,
236-
}}
237-
>
238-
{set.done ? (
239-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
240-
<path
241-
d="M5 12l5 5L20 7"
242-
stroke="#fff"
243-
strokeWidth="3"
244-
strokeLinecap="round"
245-
strokeLinejoin="round"
246-
/>
247-
</svg>
248-
) : (
249-
String(number).padStart(2, "0")
250-
)}
251-
</div>
252-
253220
<div style={{ flex: 1, minWidth: 0 }}>
254221
<div
255222
style={{
@@ -345,7 +312,6 @@ function SetRow({ set, number, onWeightChange, onToggleDone }: SetRowProps) {
345312
display: "flex",
346313
flexWrap: "wrap",
347314
gap: 3,
348-
paddingLeft: 40,
349315
}}
350316
>
351317
{lib.primary.map((m) => (

src/components/settings-screen.tsx

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
import { useState } from "react";
1+
import { useRef, useState } from "react";
2+
import type { Workout } from "@/types/workout";
23

34
type Props = {
45
bodyweight: number;
56
setBodyweight: (v: number) => void;
67
motivations: string[];
78
setMotivations: (v: string[]) => void;
9+
workouts: Workout[];
10+
setWorkouts: (v: Workout[]) => void;
811
};
912

10-
export function SettingsScreen({ bodyweight, setBodyweight, motivations, setMotivations }: Props) {
13+
export function SettingsScreen({
14+
bodyweight,
15+
setBodyweight,
16+
motivations,
17+
setMotivations,
18+
workouts,
19+
setWorkouts,
20+
}: Props) {
1121
const [editing, setEditing] = useState(false);
1222
const [draft, setDraft] = useState(String(bodyweight));
23+
const [importStatus, setImportStatus] = useState<"ok" | "error" | null>(null);
24+
const fileInputRef = useRef<HTMLInputElement>(null);
1325

1426
const lowEnd = Math.round(bodyweight * 1.6);
1527
const highEnd = Math.round(bodyweight * 2.0);
@@ -32,6 +44,47 @@ export function SettingsScreen({ bodyweight, setBodyweight, motivations, setMoti
3244
};
3345
const removeMotivation = (i: number) => setMotivations(motivations.filter((_, j) => j !== i));
3446

47+
const handleExport = () => {
48+
const json = JSON.stringify(
49+
{ version: 1, exportedAt: Date.now(), workouts, bodyweight, motivations },
50+
null,
51+
2,
52+
);
53+
const a = document.createElement("a");
54+
a.href = URL.createObjectURL(new Blob([json], { type: "application/json" }));
55+
a.download = `trainer-backup-${new Date().toISOString().slice(0, 10)}.json`;
56+
a.click();
57+
URL.revokeObjectURL(a.href);
58+
};
59+
60+
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
61+
const file = e.target.files?.[0];
62+
if (!file) return;
63+
const reader = new FileReader();
64+
reader.onload = (ev) => {
65+
try {
66+
const data = JSON.parse(ev.target?.result as string);
67+
if (
68+
data.version !== 1 ||
69+
!Array.isArray(data.workouts) ||
70+
typeof data.bodyweight !== "number" ||
71+
!Array.isArray(data.motivations)
72+
) {
73+
throw new Error("invalid");
74+
}
75+
setWorkouts(data.workouts as Workout[]);
76+
setBodyweight(data.bodyweight as number);
77+
setMotivations(data.motivations as string[]);
78+
setImportStatus("ok");
79+
} catch {
80+
setImportStatus("error");
81+
}
82+
e.target.value = "";
83+
setTimeout(() => setImportStatus(null), 3000);
84+
};
85+
reader.readAsText(file);
86+
};
87+
3588
return (
3689
<div style={{ padding: "0 0 120px", minHeight: "100%", background: "#f3f3ee" }}>
3790
<div style={{ padding: "16px 20px 0" }}>
@@ -348,6 +401,82 @@ export function SettingsScreen({ bodyweight, setBodyweight, motivations, setMoti
348401
)}
349402
</div>
350403
</div>
404+
405+
<div style={{ padding: "28px 20px 0" }}>
406+
<div
407+
style={{
408+
fontFamily: "var(--mono-font)",
409+
fontSize: 11,
410+
fontWeight: 500,
411+
letterSpacing: 2,
412+
color: "#999",
413+
textTransform: "uppercase",
414+
marginBottom: 10,
415+
}}
416+
>
417+
DATA
418+
</div>
419+
<div style={{ display: "flex", gap: 8 }}>
420+
<button
421+
onClick={handleExport}
422+
style={{
423+
flex: 1,
424+
padding: "12px 0",
425+
background: "#0a0a0a",
426+
color: "#fff",
427+
border: "1.5px solid #0a0a0a",
428+
borderRadius: 999,
429+
fontFamily: "var(--display-font)",
430+
fontSize: 12,
431+
fontWeight: 800,
432+
letterSpacing: 0.8,
433+
textTransform: "uppercase",
434+
}}
435+
>
436+
Export JSON
437+
</button>
438+
<button
439+
onClick={() => fileInputRef.current?.click()}
440+
style={{
441+
flex: 1,
442+
padding: "12px 0",
443+
background: "#fff",
444+
color: "#0a0a0a",
445+
border: "1.5px dashed #0a0a0a",
446+
borderRadius: 999,
447+
fontFamily: "var(--display-font)",
448+
fontSize: 12,
449+
fontWeight: 800,
450+
letterSpacing: 0.8,
451+
textTransform: "uppercase",
452+
}}
453+
>
454+
Import JSON
455+
</button>
456+
<input
457+
ref={fileInputRef}
458+
type="file"
459+
accept=".json,application/json"
460+
onChange={handleImport}
461+
style={{ display: "none" }}
462+
/>
463+
</div>
464+
{importStatus && (
465+
<div
466+
style={{
467+
marginTop: 8,
468+
fontFamily: "var(--mono-font)",
469+
fontSize: 11,
470+
fontWeight: 700,
471+
letterSpacing: 1.5,
472+
color: importStatus === "ok" ? "#52c878" : "#9b3919",
473+
textAlign: "center",
474+
}}
475+
>
476+
{importStatus === "ok" ? "IMPORTED ✓" : "INVALID FILE"}
477+
</div>
478+
)}
479+
</div>
351480
</div>
352481
);
353482
}

src/components/workouts-screen.tsx

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from "react";
2-
import { aggregateMuscles, relativeDate } from "@/lib/exercises";
2+
import { relativeDate } from "@/lib/exercises";
33
import type { Workout } from "@/types/workout";
4-
import { MuscleChip } from "./muscle-chip";
4+
import { Play } from "lucide-react";
55

66
type Props = {
77
workouts: Workout[];
@@ -153,7 +153,6 @@ export function WorkoutsScreen({
153153
<WorkoutCard
154154
key={w.id}
155155
workout={w}
156-
index={i}
157156
urgent={i === 0}
158157
manageMode={manageMode}
159158
onStart={() => onStart(w)}
@@ -168,18 +167,16 @@ export function WorkoutsScreen({
168167

169168
type CardProps = {
170169
workout: Workout;
171-
index: number;
172170
urgent: boolean;
173171
manageMode: boolean;
174172
onStart: () => void;
175173
onEdit: () => void;
176174
onDelete: () => void;
177175
};
178176

179-
function WorkoutCard({ workout, index, urgent, manageMode, onStart, onEdit, onDelete }: CardProps) {
177+
function WorkoutCard({ workout, urgent, manageMode, onStart, onEdit, onDelete }: CardProps) {
180178
const rel = relativeDate(workout.lastDone);
181179
const isOverdue = workout.lastDone == null || Date.now() - workout.lastDone > 7 * 86400000;
182-
const muscles = aggregateMuscles(workout.sets);
183180
const highlight = urgent && !manageMode;
184181

185182
return (
@@ -199,26 +196,6 @@ function WorkoutCard({ workout, index, urgent, manageMode, onStart, onEdit, onDe
199196
}}
200197
>
201198
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
202-
<div
203-
style={{
204-
width: 32,
205-
height: 32,
206-
borderRadius: 8,
207-
background: highlight ? "#ff5a1f" : "#f3f3ee",
208-
color: highlight ? "#fff" : "#0a0a0a",
209-
border: highlight ? "none" : "1.5px solid #0a0a0a",
210-
display: "flex",
211-
alignItems: "center",
212-
justifyContent: "center",
213-
fontFamily: "var(--mono-font)",
214-
fontSize: 12,
215-
fontWeight: 700,
216-
flexShrink: 0,
217-
}}
218-
>
219-
{String(index + 1).padStart(2, "0")}
220-
</div>
221-
222199
<div style={{ flex: 1, minWidth: 0 }}>
223200
<div
224201
style={{
@@ -257,7 +234,7 @@ function WorkoutCard({ workout, index, urgent, manageMode, onStart, onEdit, onDe
257234
background: isOverdue ? "#ff5a1f" : "#52c878",
258235
}}
259236
/>
260-
{rel.toUpperCase()} · {workout.sets.length} SETS
237+
{rel.toUpperCase()}
261238
</div>
262239
</div>
263240

@@ -329,13 +306,15 @@ function WorkoutCard({ workout, index, urgent, manageMode, onStart, onEdit, onDe
329306
}}
330307
title="Start workout"
331308
>
332-
<svg width="20" height="20" viewBox="0 0 24 24" fill="#fff" style={{ marginLeft: 3 }}>
333-
<path d="M6 4l14 8-14 8V4z" />
334-
</svg>
309+
<Play
310+
style={{
311+
color: "white",
312+
}}
313+
/>
335314
</button>
336315
)}
337316
</div>
338-
{muscles.length > 0 && (
317+
{/* {muscles.length > 0 && (
339318
<div
340319
style={{
341320
display: "flex",
@@ -348,7 +327,7 @@ function WorkoutCard({ workout, index, urgent, manageMode, onStart, onEdit, onDe
348327
<MuscleChip key={m} muscle={m} size="sm" />
349328
))}
350329
</div>
351-
)}
330+
)} */}
352331
</div>
353332
);
354333
}

src/lib/exercises.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ export const EXERCISE_LIBRARY: Exercise[] = [
2626
primary: ["Lats"],
2727
secondary: ["Biceps", "Rhomboids", "Trapezius", "Rear Deltoids", "Forearms"],
2828
},
29+
{
30+
name: "Push Ups",
31+
primary: ["Chest"],
32+
secondary: ["Triceps", "Anterior Deltoids", "Serratus Anterior", "Core"],
33+
},
34+
{
35+
name: "Biceps Curls",
36+
primary: ["Biceps"],
37+
secondary: ["Brachialis", "Forearms"],
38+
},
2939
];
3040

3141
const LIBRARY_INDEX = new Map(EXERCISE_LIBRARY.map((e) => [e.name.toLowerCase(), e]));

0 commit comments

Comments
 (0)