From 53f3cec385b88c4c1d59fab0dbe59de47790892e Mon Sep 17 00:00:00 2001 From: masongga Date: Sat, 23 May 2026 22:12:00 -0600 Subject: [PATCH] tasks now update streaks in db --- lib/daily-quiz-storage.ts | 9 +- lib/habits-storage.ts | 86 +++++++++++++------ .../20260524160000_deduplicate_habits.sql | 13 +++ 3 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 supabase/migrations/20260524160000_deduplicate_habits.sql diff --git a/lib/daily-quiz-storage.ts b/lib/daily-quiz-storage.ts index fa30185..b1df525 100644 --- a/lib/daily-quiz-storage.ts +++ b/lib/daily-quiz-storage.ts @@ -69,17 +69,20 @@ export async function getDailyEntryForToday(): Promise(rows: T[] | null | undefined) { + return rows?.[0] ?? null; +} + +function dedupeHabitsByName(rows: HabitRow[]) { + const seen = new Map(); + + for (const row of rows) { + const key = row.name.trim().toLowerCase(); + + if (!seen.has(key)) { + seen.set(key, row); + } + } + + return Array.from(seen.values()); +} + +async function findActiveHabitByName( + userId: string, + name: string, +): Promise { + const supabase = createClient(); + const { data, error } = await supabase + .from("habits") + .select("id, name, streak, last_completed_on") + .eq("user_id", userId) + .eq("name", name) + .eq("active", true) + .order("created_at", { ascending: true }); + + if (error) { + throw new Error(error.message); + } + + return getFirstRow(data as HabitRow[] | null); +} + async function getAuthenticatedUserId() { const supabase = createClient(); const { @@ -111,14 +149,13 @@ async function getPreviousCompletedDate( .eq("habit_id", habitId) .neq("completed_on", today) .order("completed_on", { ascending: false }) - .limit(1) - .maybeSingle(); + .limit(1); if (error) { throw new Error(error.message); } - return data?.completed_on ?? null; + return getFirstRow(data as { completed_on: string }[] | null)?.completed_on ?? null; } export function isHabitCompletedToday(habit: Habit, today = getTodayDateKey()) { @@ -144,7 +181,9 @@ export async function getHabits(): Promise { throw new Error(error.message); } - const rows = await reconcileMissedDayStreaks((data ?? []) as HabitRow[]); + const rows = await reconcileMissedDayStreaks( + dedupeHabitsByName((data ?? []) as HabitRow[]), + ); return rows.map(mapHabitRow); } @@ -163,19 +202,7 @@ export async function ensureCatalogHabit(catalogId: string): Promise { const [reconciledHabit] = await reconcileMissedDayStreaks([habit as HabitRow]); const currentStreak = reconciledHabit.streak; - const { data: existingLog, error: existingLogError } = await supabase + const { data: existingLogs, error: existingLogError } = await supabase .from("habit_logs") .select("id") .eq("user_id", userId) .eq("habit_id", habitId) - .eq("completed_on", today) - .maybeSingle(); + .eq("completed_on", today); if (existingLogError) { throw new Error(existingLogError.message); } + const existingLog = getFirstRow(existingLogs as { id: string }[] | null); + if (existingLog) { const { error: deleteError } = await supabase .from("habit_logs") .delete() - .eq("id", existingLog.id); + .eq("user_id", userId) + .eq("habit_id", habitId) + .eq("completed_on", today); if (deleteError) { throw new Error(deleteError.message); diff --git a/supabase/migrations/20260524160000_deduplicate_habits.sql b/supabase/migrations/20260524160000_deduplicate_habits.sql new file mode 100644 index 0000000..4b7bbaa --- /dev/null +++ b/supabase/migrations/20260524160000_deduplicate_habits.sql @@ -0,0 +1,13 @@ +-- Remove duplicate active habits with the same name for a user. +delete from public.habits duplicate +using public.habits keeper +where duplicate.user_id = keeper.user_id + and duplicate.name = keeper.name + and duplicate.active = true + and keeper.active = true + and duplicate.id <> keeper.id + and duplicate.created_at > keeper.created_at; + +create unique index if not exists habits_user_id_name_active_unique + on public.habits (user_id, name) + where active = true;