adding habit_streak_milestones
This commit is contained in:
@@ -1,34 +1,5 @@
|
|||||||
import { AppShell } from "@/components/layout/app-shell";
|
import { AppShell } from "@/components/layout/app-shell";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { ShopContent } from "@/components/shop/shop-content";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
const shopItems = [
|
|
||||||
{
|
|
||||||
id: "hat",
|
|
||||||
name: "Pixel cap",
|
|
||||||
price: 50,
|
|
||||||
description: "A cozy cap for your habit pet.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bed",
|
|
||||||
name: "Cloud bed",
|
|
||||||
price: 120,
|
|
||||||
description: "Boost recovery after tough days.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "snack",
|
|
||||||
name: "Berry snack",
|
|
||||||
price: 25,
|
|
||||||
description: "A small treat for good streaks.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ShopPage() {
|
export default function ShopPage() {
|
||||||
return (
|
return (
|
||||||
@@ -36,31 +7,7 @@ export default function ShopPage() {
|
|||||||
title="Shop"
|
title="Shop"
|
||||||
description="Spend points earned from habits and daily check-ins."
|
description="Spend points earned from habits and daily check-ins."
|
||||||
>
|
>
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border bg-muted/40 px-4 py-3">
|
<ShopContent />
|
||||||
<span className="text-sm text-muted-foreground">Your balance</span>
|
|
||||||
<Badge variant="secondary">120 points</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{shopItems.map((item) => (
|
|
||||||
<Card key={item.id}>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-base">{item.name}</CardTitle>
|
|
||||||
<CardDescription>{item.description}</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Badge>{item.price} pts</Badge>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button className="w-full" variant="outline">
|
|
||||||
Buy
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
toggleHabitCompletion,
|
toggleHabitCompletion,
|
||||||
type Habit,
|
type Habit,
|
||||||
} from "@/lib/habits-storage";
|
} from "@/lib/habits-storage";
|
||||||
|
import { formatCoinsDelta } from "@/lib/coins";
|
||||||
import { getProfilePreferences } from "@/lib/profile-preferences-storage";
|
import { getProfilePreferences } from "@/lib/profile-preferences-storage";
|
||||||
import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage";
|
import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage";
|
||||||
|
|
||||||
@@ -187,6 +188,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
() => new Set(),
|
() => new Set(),
|
||||||
);
|
);
|
||||||
const [toggleErrors, setToggleErrors] = useState<Record<string, string>>({});
|
const [toggleErrors, setToggleErrors] = useState<Record<string, string>>({});
|
||||||
|
const [coinMessage, setCoinMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const getIsCompleted = useCallback(
|
const getIsCompleted = useCallback(
|
||||||
(habit: DailyTask) => {
|
(habit: DailyTask) => {
|
||||||
@@ -231,6 +233,20 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!coinMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setCoinMessage(null);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [coinMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|
||||||
@@ -262,7 +278,8 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const updatedHabits = await toggleHabitCompletion(habitId);
|
const { habits: updatedHabits, coinsDelta } =
|
||||||
|
await toggleHabitCompletion(habitId);
|
||||||
|
|
||||||
setDailyTasks((current) =>
|
setDailyTasks((current) =>
|
||||||
current.map((task) => {
|
current.map((task) => {
|
||||||
@@ -275,6 +292,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
delete next[habitId];
|
delete next[habitId];
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
setCoinMessage(formatCoinsDelta(coinsDelta));
|
||||||
} catch (toggleError) {
|
} catch (toggleError) {
|
||||||
setOptimisticCompletion((current) => {
|
setOptimisticCompletion((current) => {
|
||||||
const next = { ...current };
|
const next = { ...current };
|
||||||
@@ -416,6 +434,9 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
|
{coinMessage ? (
|
||||||
|
<p className="text-sm font-medium text-emerald-600">{coinMessage}</p>
|
||||||
|
) : null}
|
||||||
<DailyTaskList
|
<DailyTaskList
|
||||||
tasks={dailyTasks}
|
tasks={dailyTasks}
|
||||||
getIsCompleted={getIsCompleted}
|
getIsCompleted={getIsCompleted}
|
||||||
|
|||||||
106
components/shop/shop-content.tsx
Normal file
106
components/shop/shop-content.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||||
|
import { getCoins } from "@/lib/avatar-progression-storage";
|
||||||
|
|
||||||
|
const shopItems = [
|
||||||
|
{
|
||||||
|
id: "hat",
|
||||||
|
name: "Pixel cap",
|
||||||
|
price: 50,
|
||||||
|
description: "A cozy cap for your habit pet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bed",
|
||||||
|
name: "Cloud bed",
|
||||||
|
price: 120,
|
||||||
|
description: "Boost recovery after tough days.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "snack",
|
||||||
|
name: "Berry snack",
|
||||||
|
price: 25,
|
||||||
|
description: "A small treat for good streaks.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ShopContent() {
|
||||||
|
const [coins, setCoins] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refreshCoins = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setCoins(await getCoins());
|
||||||
|
} catch (refreshError) {
|
||||||
|
setError(
|
||||||
|
refreshError instanceof Error
|
||||||
|
? refreshError.message
|
||||||
|
: "Could not load your balance.",
|
||||||
|
);
|
||||||
|
setCoins(0);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshCoins();
|
||||||
|
|
||||||
|
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refreshCoins);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refreshCoins);
|
||||||
|
};
|
||||||
|
}, [refreshCoins]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 flex items-center justify-between rounded-lg border bg-muted/40 px-4 py-3">
|
||||||
|
<span className="text-sm text-muted-foreground">Your balance</span>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{coins === null ? "..." : `${coins} points`}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <p className="mb-4 text-sm text-red-500">{error}</p> : null}
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{shopItems.map((item) => {
|
||||||
|
const canAfford = coins !== null && coins >= item.price;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={item.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base">{item.name}</CardTitle>
|
||||||
|
<CardDescription>{item.description}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Badge>{item.price} pts</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant="outline"
|
||||||
|
disabled={coins === null || !canAfford}
|
||||||
|
>
|
||||||
|
{canAfford ? "Buy" : "Not enough points"}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
lib/avatar-progression-storage.ts
Normal file
74
lib/avatar-progression-storage.ts
Normal 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
28
lib/coins.ts
Normal 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`;
|
||||||
|
}
|
||||||
@@ -4,7 +4,13 @@ import {
|
|||||||
getCatalogIdByLabel,
|
getCatalogIdByLabel,
|
||||||
isCatalogLabel,
|
isCatalogLabel,
|
||||||
} from "@/lib/habit-catalog";
|
} from "@/lib/habit-catalog";
|
||||||
|
import { adjustCoins } from "@/lib/avatar-progression-storage";
|
||||||
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
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";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
export type Habit = {
|
export type Habit = {
|
||||||
@@ -16,13 +22,22 @@ export type Habit = {
|
|||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ToggleHabitResult = {
|
||||||
|
habits: Habit[];
|
||||||
|
coinsDelta: number;
|
||||||
|
};
|
||||||
|
|
||||||
type HabitRow = {
|
type HabitRow = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
streak: number;
|
streak: number;
|
||||||
last_completed_on: string | null;
|
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()) {
|
export function getTodayDateKey(date = new Date()) {
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -81,7 +96,7 @@ async function findActiveHabitByName(
|
|||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("habits")
|
.from("habits")
|
||||||
.select("id, name, streak, last_completed_on")
|
.select(habitSelectFields)
|
||||||
.eq("user_id", userId)
|
.eq("user_id", userId)
|
||||||
.eq("name", name)
|
.eq("name", name)
|
||||||
.eq("active", true)
|
.eq("active", true)
|
||||||
@@ -125,13 +140,16 @@ async function reconcileMissedDayStreaks(
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
habitsToReset.map((habit) =>
|
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) =>
|
return habits.map((habit) =>
|
||||||
habitsToReset.some((item) => item.id === habit.id)
|
habitsToReset.some((item) => item.id === habit.id)
|
||||||
? { ...habit, streak: 0 }
|
? { ...habit, streak: 0, claimed_streak_milestones: [] }
|
||||||
: habit,
|
: habit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -172,7 +190,7 @@ export async function getHabits(): Promise<Habit[]> {
|
|||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("habits")
|
.from("habits")
|
||||||
.select("id, name, streak, last_completed_on")
|
.select(habitSelectFields)
|
||||||
.eq("user_id", userId)
|
.eq("user_id", userId)
|
||||||
.eq("active", true)
|
.eq("active", true)
|
||||||
.order("created_at", { ascending: true });
|
.order("created_at", { ascending: true });
|
||||||
@@ -213,7 +231,7 @@ export async function ensureCatalogHabit(catalogId: string): Promise<Habit | nul
|
|||||||
active: true,
|
active: true,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
})
|
})
|
||||||
.select("id, name, streak, last_completed_on")
|
.select(habitSelectFields)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (insertError) {
|
if (insertError) {
|
||||||
@@ -231,11 +249,13 @@ export async function ensureCatalogHabit(catalogId: string): Promise<Habit | nul
|
|||||||
return mapHabitRow(reconciledRow);
|
return mapHabitRow(reconciledRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
export async function toggleHabitCompletion(
|
||||||
|
habitId: string,
|
||||||
|
): Promise<ToggleHabitResult> {
|
||||||
const userId = await getAuthenticatedUserId();
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return [];
|
return { habits: [], coinsDelta: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
@@ -243,7 +263,7 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
|||||||
|
|
||||||
const { data: habit, error: habitError } = await supabase
|
const { data: habit, error: habitError } = await supabase
|
||||||
.from("habits")
|
.from("habits")
|
||||||
.select("id, name, streak, last_completed_on")
|
.select(habitSelectFields)
|
||||||
.eq("id", habitId)
|
.eq("id", habitId)
|
||||||
.eq("user_id", userId)
|
.eq("user_id", userId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
@@ -271,6 +291,7 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingLog = getFirstRow(existingLogs as { id: string }[] | null);
|
const existingLog = getFirstRow(existingLogs as { id: string }[] | null);
|
||||||
|
let coinsDelta = 0;
|
||||||
|
|
||||||
if (existingLog) {
|
if (existingLog) {
|
||||||
const { error: deleteError } = await supabase
|
const { error: deleteError } = await supabase
|
||||||
@@ -299,6 +320,8 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
|||||||
if (updateError) {
|
if (updateError) {
|
||||||
throw new Error(updateError.message);
|
throw new Error(updateError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coinsDelta = COINS_PER_TASK_UNCHECK;
|
||||||
} else {
|
} else {
|
||||||
const { error: insertError } = await supabase.from("habit_logs").insert({
|
const { error: insertError } = await supabase.from("habit_logs").insert({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
@@ -311,12 +334,20 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextStreak = currentStreak + 1;
|
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
|
const { error: updateError } = await supabase
|
||||||
.from("habits")
|
.from("habits")
|
||||||
.update({
|
.update({
|
||||||
streak: nextStreak,
|
streak: nextStreak,
|
||||||
last_completed_on: today,
|
last_completed_on: today,
|
||||||
|
claimed_streak_milestones: nextClaimedMilestones,
|
||||||
})
|
})
|
||||||
.eq("id", habitId)
|
.eq("id", habitId)
|
||||||
.eq("user_id", userId);
|
.eq("user_id", userId);
|
||||||
@@ -324,10 +355,15 @@ export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
|||||||
if (updateError) {
|
if (updateError) {
|
||||||
throw new Error(updateError.message);
|
throw new Error(updateError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coinsDelta =
|
||||||
|
COINS_PER_TASK_COMPLETE + (earnedMilestoneBonus ? milestoneBonus : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await adjustCoins(coinsDelta);
|
||||||
|
|
||||||
notifyHabitPetDataUpdated();
|
notifyHabitPetDataUpdated();
|
||||||
return getHabits();
|
return { habits: await getHabits(), coinsDelta };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addHabit(label: string): Promise<Habit[]> {
|
export async function addHabit(label: string): Promise<Habit[]> {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table public.habits
|
||||||
|
add column if not exists claimed_streak_milestones int[] not null default '{}';
|
||||||
Reference in New Issue
Block a user