adding habit_streak_milestones

This commit is contained in:
masongga
2026-05-23 22:18:28 -06:00
parent 53f3cec385
commit 4d22c04f83
7 changed files with 279 additions and 65 deletions

View File

@@ -0,0 +1,74 @@
import { createClient } from "@/lib/supabase/client";
async function getAuthenticatedUserId() {
const supabase = createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error || !user) {
return null;
}
return user.id;
}
export async function getCoins(): Promise<number> {
const userId = await getAuthenticatedUserId();
if (!userId) {
return 0;
}
const supabase = createClient();
const { data, error } = await supabase
.from("avatar_state")
.select("coins")
.eq("user_id", userId)
.maybeSingle();
if (error) {
throw new Error(error.message);
}
if (!data) {
const { error: insertError } = await supabase
.from("avatar_state")
.insert({ user_id: userId, coins: 0 });
if (insertError) {
throw new Error(insertError.message);
}
return 0;
}
return data.coins ?? 0;
}
export async function adjustCoins(delta: number): Promise<number> {
const userId = await getAuthenticatedUserId();
if (!userId) {
throw new Error("You must be signed in to earn points.");
}
const currentCoins = await getCoins();
const nextCoins = Math.max(0, currentCoins + delta);
const supabase = createClient();
const { error } = await supabase
.from("avatar_state")
.update({
coins: nextCoins,
updated_at: new Date().toISOString(),
})
.eq("user_id", userId);
if (error) {
throw new Error(error.message);
}
return nextCoins;
}

28
lib/coins.ts Normal file
View File

@@ -0,0 +1,28 @@
export const COINS_PER_TASK_COMPLETE = 50;
export const COINS_PER_TASK_UNCHECK = -50;
export const STREAK_MILESTONE_COINS: Record<number, number> = {
5: 50,
7: 75,
10: 100,
20: 150,
30: 200,
50: 350,
100: 750,
};
export const STREAK_MILESTONES = Object.keys(STREAK_MILESTONE_COINS)
.map(Number)
.sort((a, b) => a - b);
export function getStreakMilestoneBonus(streak: number) {
return STREAK_MILESTONE_COINS[streak] ?? null;
}
export function formatCoinsDelta(delta: number) {
if (delta === 0) {
return "0 points";
}
return `${delta > 0 ? "+" : ""}${delta} points`;
}

View File

@@ -4,7 +4,13 @@ import {
getCatalogIdByLabel,
isCatalogLabel,
} from "@/lib/habit-catalog";
import { adjustCoins } from "@/lib/avatar-progression-storage";
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
import {
COINS_PER_TASK_COMPLETE,
COINS_PER_TASK_UNCHECK,
getStreakMilestoneBonus,
} from "@/lib/coins";
import { createClient } from "@/lib/supabase/client";
export type Habit = {
@@ -16,13 +22,22 @@ export type Habit = {
isCustom: boolean;
};
export type ToggleHabitResult = {
habits: Habit[];
coinsDelta: number;
};
type HabitRow = {
id: string;
name: string;
streak: number;
last_completed_on: string | null;
claimed_streak_milestones?: number[];
};
const habitSelectFields =
"id, name, streak, last_completed_on, claimed_streak_milestones";
export function getTodayDateKey(date = new Date()) {
return date.toISOString().slice(0, 10);
}
@@ -81,7 +96,7 @@ async function findActiveHabitByName(
const supabase = createClient();
const { data, error } = await supabase
.from("habits")
.select("id, name, streak, last_completed_on")
.select(habitSelectFields)
.eq("user_id", userId)
.eq("name", name)
.eq("active", true)
@@ -125,13 +140,16 @@ async function reconcileMissedDayStreaks(
await Promise.all(
habitsToReset.map((habit) =>
supabase.from("habits").update({ streak: 0 }).eq("id", habit.id),
supabase
.from("habits")
.update({ streak: 0, claimed_streak_milestones: [] })
.eq("id", habit.id),
),
);
return habits.map((habit) =>
habitsToReset.some((item) => item.id === habit.id)
? { ...habit, streak: 0 }
? { ...habit, streak: 0, claimed_streak_milestones: [] }
: habit,
);
}
@@ -172,7 +190,7 @@ export async function getHabits(): Promise<Habit[]> {
const supabase = createClient();
const { data, error } = await supabase
.from("habits")
.select("id, name, streak, last_completed_on")
.select(habitSelectFields)
.eq("user_id", userId)
.eq("active", true)
.order("created_at", { ascending: true });
@@ -213,7 +231,7 @@ export async function ensureCatalogHabit(catalogId: string): Promise<Habit | nul
active: true,
streak: 0,
})
.select("id, name, streak, last_completed_on")
.select(habitSelectFields)
.single();
if (insertError) {
@@ -231,11 +249,13 @@ export async function ensureCatalogHabit(catalogId: string): Promise<Habit | nul
return mapHabitRow(reconciledRow);
}
export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
export async function toggleHabitCompletion(
habitId: string,
): Promise<ToggleHabitResult> {
const userId = await getAuthenticatedUserId();
if (!userId) {
return [];
return { habits: [], coinsDelta: 0 };
}
const supabase = createClient();
@@ -243,7 +263,7 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
const { data: habit, error: habitError } = await supabase
.from("habits")
.select("id, name, streak, last_completed_on")
.select(habitSelectFields)
.eq("id", habitId)
.eq("user_id", userId)
.maybeSingle();
@@ -271,6 +291,7 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
}
const existingLog = getFirstRow(existingLogs as { id: string }[] | null);
let coinsDelta = 0;
if (existingLog) {
const { error: deleteError } = await supabase
@@ -299,6 +320,8 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
if (updateError) {
throw new Error(updateError.message);
}
coinsDelta = COINS_PER_TASK_UNCHECK;
} else {
const { error: insertError } = await supabase.from("habit_logs").insert({
user_id: userId,
@@ -311,12 +334,20 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
}
const nextStreak = currentStreak + 1;
const claimedMilestones = reconciledHabit.claimed_streak_milestones ?? [];
const milestoneBonus = getStreakMilestoneBonus(nextStreak);
const earnedMilestoneBonus =
milestoneBonus !== null && !claimedMilestones.includes(nextStreak);
const nextClaimedMilestones = earnedMilestoneBonus
? [...claimedMilestones, nextStreak]
: claimedMilestones;
const { error: updateError } = await supabase
.from("habits")
.update({
streak: nextStreak,
last_completed_on: today,
claimed_streak_milestones: nextClaimedMilestones,
})
.eq("id", habitId)
.eq("user_id", userId);
@@ -324,10 +355,15 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
if (updateError) {
throw new Error(updateError.message);
}
coinsDelta =
COINS_PER_TASK_COMPLETE + (earnedMilestoneBonus ? milestoneBonus : 0);
}
await adjustCoins(coinsDelta);
notifyHabitPetDataUpdated();
return getHabits();
return { habits: await getHabits(), coinsDelta };
}
export async function addHabit(label: string): Promise<Habit[]> {