Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
aa11312
feat: enhance workout duration estimation with unilateral exercise ha…
isotronic May 20, 2026
9afa2b2
feat: integrate workout duration estimation with unilateral support …
isotronic May 20, 2026
cb912d9
feat: implement draft management for workout and plan creation, inclu…
isotronic May 20, 2026
641ab38
fix: improve workout completion logic to handle undefined target values
isotronic May 20, 2026
a10704c
feat: add weekly workout streak tracking and insights to StatsScreen
isotronic May 20, 2026
bc08d23
fix: clean up StatsTile component structure and improve readability
isotronic May 20, 2026
5456e60
feat: enhance InsightsStrip layout with grid support for better pill …
isotronic May 20, 2026
d072d59
feat: separate warmup and working sets in sets overview
isotronic May 20, 2026
d7c7fad
fix: adjust tooltip handling in InsightsStrip for improved pill refer…
isotronic May 21, 2026
e169428
fix: remove tilde from single duration estimate formatting
isotronic May 21, 2026
46dead9
fix: clone last set properties when inserting into exercise sets
isotronic May 21, 2026
f8705aa
fix: handle undefined target in workout completion check
isotronic May 21, 2026
b15ea14
fix: enhance error handling in StatsScreen to include allWorkouts que…
isotronic May 21, 2026
98e7e59
fix: update set handling in EditSetModal to apply changes to specific…
isotronic May 21, 2026
03580c0
feat: improve draft handling in CreateWorkoutScreen and CreatePlanScr…
isotronic May 21, 2026
b352e83
fix: update tooltip text in InsightsStrip to include 1RM for biggest …
isotronic May 21, 2026
b4d385d
fix: enhance StatsTile to support deltaText for improved delta display
isotronic May 21, 2026
c664065
style: improve formatting in WorkoutHistoryCard and WorkoutHistorySec…
isotronic May 21, 2026
03cb6e2
fix: improve set index handling in ActiveWorkoutStore for better exer…
isotronic May 21, 2026
5679723
fix: improve animation handling in WorkoutSessionScreen for better st…
isotronic May 21, 2026
74b622b
refactor: replace deprecated runOnJS with scheduleOnRN
isotronic May 21, 2026
3af5e0c
fix: optimize completion check logic in useActiveWorkoutStore
isotronic May 21, 2026
8b15cad
fix: remove unnecessary rest timer logic in WorkoutSessionScreen
isotronic May 21, 2026
a799dc4
feat: add new plans to userData.db and ensure exercises exist in DB
isotronic May 21, 2026
c5b745a
fix: throw error if exercise not found in userData.db during plan ins…
isotronic May 21, 2026
a7bf1a5
test: enhance loadPremadePlans tests for exercise ID translation and …
isotronic May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 80 additions & 5 deletions app/(app)/(create-plan)/create-workout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,85 @@ export default function CreateWorkoutScreen() {
addWorkout,
changeWorkoutName,
setWorkouts,
saveDraftEntry,
clearDraftEntry,
clearDraft,
} = useWorkoutStore();

const draftKey =
existingWorkoutId !== null
? `standalone:${existingWorkoutId}`
: "standalone:null";

const { data: standaloneWorkouts } = useStandaloneWorkoutsQuery();
const createMutation = useCreateStandaloneWorkout();
const updateMutation = useUpdateStandaloneWorkout();

// 'pending' until draft check resolves; then 'continue' or 'fresh'
Comment thread
isotronic marked this conversation as resolved.
const [draftDecision, setDraftDecision] = useState<
"pending" | "continue" | "fresh"
>("pending");

// Check for a persisted draft once on mount, after store hydrates
useEffect(() => {
const checkDraft = () => {
const { drafts } = useWorkoutStore.getState();
const draft = drafts[draftKey];
const hasDraft =
!!draft &&
draft.workouts.length > 0 &&
(draft.workouts[0].exercises.length > 0 ||
draft.workouts[0].name.trim() !== "");

if (hasDraft) {
Alert.alert(
"Continue Editing?",
"You have unsaved changes from your last session. Would you like to continue?",
[
{
text: "Discard Changes",
style: "destructive",
onPress: () => {
clearDraftEntry(draftKey);
clearDraft();
setDraftDecision("fresh");
},
},
{
text: "Continue",
onPress: () => {
setWorkouts(draft.workouts);
initializedWorkoutId.current = existingWorkoutId ?? null;
setDraftDecision("continue");
},
},
],
);
} else {
setDraftDecision("fresh");
}
};

if (useWorkoutStore.persist.hasHydrated()) {
checkDraft();
} else {
const unsub = useWorkoutStore.persist.onFinishHydration(checkDraft);
return unsub;
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

// Auto-save draft on every change so it survives app close
useEffect(() => {
if (draftDecision === "pending") return;
const workout = workouts[0];
if (!workout || (!workout.exercises.length && !workout.name.trim())) return;
saveDraftEntry(draftKey, { workouts });
}, [workouts, draftDecision]); // eslint-disable-line react-hooks/exhaustive-deps

// Initialise workoutStore for this screen — runs once per workout id
useEffect(() => {
if (draftDecision === "pending" || draftDecision === "continue") return;

const sentinel = existingWorkoutId ?? null;
if (initializedWorkoutId.current === sentinel) return;
if (existingWorkoutId && standaloneWorkouts) {
Expand All @@ -72,7 +143,7 @@ export default function CreateWorkoutScreen() {
clearWorkouts();
addWorkout({ name: "", exercises: [], id: -Date.now() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingWorkoutId, standaloneWorkouts]);
}, [draftDecision, existingWorkoutId, standaloneWorkouts]);

// Guard back-navigation when there are unsaved changes
useEffect(() => {
Expand All @@ -81,13 +152,15 @@ export default function CreateWorkoutScreen() {

if (savedRef.current) {
savedRef.current = false;
clearWorkouts();
clearDraftEntry(draftKey);
clearDraft();
return navigation.dispatch(e.data.action);
}

const workout = workouts[0];
if (!workout || (!workout.exercises.length && !workout.name.trim())) {
clearWorkouts();
clearDraftEntry(draftKey);
clearDraft();
return navigation.dispatch(e.data.action);
}

Expand All @@ -100,15 +173,16 @@ export default function CreateWorkoutScreen() {
text: "Discard",
style: "destructive",
onPress: () => {
clearWorkouts();
clearDraftEntry(draftKey);
clearDraft();
navigation.dispatch(e.data.action);
},
},
],
);
});
return unsubscribe;
}, [navigation, workouts, clearWorkouts]);
}, [navigation, workouts, clearDraft, clearDraftEntry, draftKey]);

const handleAddExercise = () => {
router.push("/(app)/(create-plan)/exercises?index=0");
Expand Down Expand Up @@ -136,6 +210,7 @@ export default function CreateWorkoutScreen() {
}

await queryClient.invalidateQueries({ queryKey: ["standaloneWorkouts"] });
clearDraftEntry(draftKey);
savedRef.current = true;
router.back();
} catch (error: any) {
Expand Down
121 changes: 104 additions & 17 deletions app/(app)/(create-plan)/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,111 @@ export default function CreatePlanScreen() {
const router = useRouter();
const queryClient = useQueryClient();
const { planId } = useLocalSearchParams();
const [dataLoaded, setDataLoaded] = useState(!planId);
const [dataLoaded, setDataLoaded] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const scrollRef = useRef<ScrollViewType>(null);
const {
workouts,
planImageUrl,
setPlanImageUrl,
setWorkouts,
clearWorkouts,
addWorkout,
removeWorkout,
reorderWorkouts,
changeWorkoutName,
planSchedule,
setPlanSchedule,
clearPlanSchedule,
saveDraftEntry,
clearDraftEntry,
clearDraft,
} = useWorkoutStore();

const currentPlanId = planId ? Number(planId) : null;
const draftKey =
currentPlanId !== null ? `plan:${currentPlanId}` : "plan:null";
const { data: settings } = useSettingsQuery();
const weeklyGoal = Number(settings?.weeklyGoal ?? 3);
const { planName, setPlanName, planSaved, setPlanSaved, handleSavePlan } =
useCreatePlan();
const { data: existingPlan } = usePlanQuery(planId ? Number(planId) : null);

// 'pending' until draft check resolves; then 'continue' or 'fresh'
Comment thread
isotronic marked this conversation as resolved.
const [draftDecision, setDraftDecision] = useState<
"pending" | "continue" | "fresh"
>("pending");

// Check for a persisted draft once on mount, after store hydrates
useEffect(() => {
const checkDraft = () => {
const { drafts } = useWorkoutStore.getState();
const draft = drafts[draftKey];
const hasDraft =
!!draft &&
(draft.workouts.length > 0 || (draft.draftName ?? "").trim() !== "");

if (hasDraft) {
Alert.alert(
"Continue Editing?",
"You have unsaved changes from your last session. Would you like to continue?",
[
{
text: "Discard Changes",
style: "destructive",
onPress: () => {
clearDraftEntry(draftKey);
clearDraft();
setDraftDecision("fresh");
if (!planId) setDataLoaded(true);
},
},
{
text: "Continue",
onPress: () => {
setWorkouts(draft.workouts);
if (draft.planImageUrl) setPlanImageUrl(draft.planImageUrl);
if (draft.planSchedule) setPlanSchedule(draft.planSchedule);
setPlanName(draft.draftName ?? "");
setDraftDecision("continue");
if (!planId) setDataLoaded(true);
},
},
],
);
} else {
setDraftDecision("fresh");
if (!planId) setDataLoaded(true);
}
};

if (useWorkoutStore.persist.hasHydrated()) {
checkDraft();
} else {
const unsub = useWorkoutStore.persist.onFinishHydration(checkDraft);
return unsub;
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

// Auto-save draft on every change so it survives app close
useEffect(() => {
if (existingPlan) {
if (draftDecision === "pending") return;
if (workouts.length === 0 && !planName.trim()) return;
saveDraftEntry(draftKey, {
workouts,
planImageUrl,
planSchedule,
draftName: planName,
});
}, [workouts, planImageUrl, planSchedule, planName, draftDecision]); // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
if (draftDecision === "pending") return;
if (draftDecision === "continue") {
setDataLoaded(true);
return;
}
if (!existingPlan) return;

if (draftDecision === "fresh") {
setPlanName(existingPlan.name);
setPlanImageUrl(existingPlan.image_url);

Expand All @@ -85,7 +165,7 @@ export default function CreatePlanScreen() {
if (entries.length > 0 && existingPlan.workouts) {
const schedule: Record<number, number> = {};
for (const entry of entries) {
const idx = existingPlan.workouts.findIndex(
const idx = existingPlan.workouts!.findIndex(
(w) => w.id === entry.workout_id,
);
if (idx !== -1) {
Expand All @@ -102,18 +182,20 @@ export default function CreatePlanScreen() {
planId: existingPlan.id,
});
});
// Non-critical: schedule defaults to empty
});
}

setDataLoaded(true);
setDataLoaded(true);

return () => {
cancelled = true;
};
return () => {
cancelled = true;
};
}
}

setDataLoaded(true);
}, [
existingPlan,
draftDecision,
setPlanName,
setWorkouts,
setPlanImageUrl,
Expand All @@ -128,13 +210,15 @@ export default function CreatePlanScreen() {
if (planSaved) {
queryClient.invalidateQueries({ queryKey: ["plans"] });
queryClient.invalidateQueries({ queryKey: ["activePlan"] });
clearWorkouts();
clearPlanSchedule();
clearDraftEntry(draftKey);
clearDraft();
setPlanSaved(false);
return navigation.dispatch(e.data.action);
}

if (!workouts.length && !planName.trim()) {
clearDraftEntry(draftKey);
clearDraft();
return navigation.dispatch(e.data.action);
}

Expand All @@ -147,8 +231,8 @@ export default function CreatePlanScreen() {
text: "Discard",
style: "destructive",
onPress: () => {
clearWorkouts();
clearPlanSchedule();
clearDraftEntry(draftKey);
clearDraft();
setPlanSaved(false);
navigation.dispatch(e.data.action);
},
Expand All @@ -162,8 +246,9 @@ export default function CreatePlanScreen() {
navigation,
workouts,
planName,
clearWorkouts,
clearPlanSchedule,
clearDraft,
clearDraftEntry,
draftKey,
planSaved,
setPlanSaved,
queryClient,
Expand Down Expand Up @@ -218,6 +303,8 @@ export default function CreatePlanScreen() {
existingPlan?.app_plan_id,
);

clearDraftEntry(draftKey);

await Promise.all([
queryClient.invalidateQueries({ queryKey: ["plans"] }),
queryClient.invalidateQueries({ queryKey: ["activePlan"] }),
Expand Down
Loading
Loading