added habits to work in db
This commit is contained in:
@@ -154,18 +154,34 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
const [newHabitLabel, setNewHabitLabel] = useState("");
|
const [newHabitLabel, setNewHabitLabel] = useState("");
|
||||||
const [focusTopics, setFocusTopics] = useState<string[]>([]);
|
const [focusTopics, setFocusTopics] = useState<string[]>([]);
|
||||||
const [quizCompletedToday, setQuizCompletedToday] = useState(false);
|
const [quizCompletedToday, setQuizCompletedToday] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(async () => {
|
||||||
setDailyTasks(getDailyTasks());
|
try {
|
||||||
setCustomHabits(getCustomHabits());
|
setError(null);
|
||||||
setFocusTopics(getProfilePreferences().focusTopics);
|
const [tasks, customs] = await Promise.all([
|
||||||
setQuizCompletedToday(hasCompletedDailyQuizToday());
|
getDailyTasks(),
|
||||||
setIsReady(true);
|
getCustomHabits(),
|
||||||
|
]);
|
||||||
|
setDailyTasks(tasks);
|
||||||
|
setCustomHabits(customs);
|
||||||
|
setFocusTopics(getProfilePreferences().focusTopics);
|
||||||
|
setQuizCompletedToday(hasCompletedDailyQuizToday());
|
||||||
|
setIsReady(true);
|
||||||
|
} catch (refreshError) {
|
||||||
|
setError(
|
||||||
|
refreshError instanceof Error
|
||||||
|
? refreshError.message
|
||||||
|
: "Could not load habits.",
|
||||||
|
);
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
void refresh();
|
||||||
|
|
||||||
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -173,17 +189,51 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
};
|
};
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
const handleToggle = (habitId: string) => {
|
const handleToggle = async (habitId: string) => {
|
||||||
toggleHabitCompletion(habitId);
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
await toggleHabitCompletion(habitId);
|
||||||
|
} catch (toggleError) {
|
||||||
|
setError(
|
||||||
|
toggleError instanceof Error
|
||||||
|
? toggleError.message
|
||||||
|
: "Could not update habit.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddHabit = () => {
|
const handleAddHabit = async () => {
|
||||||
addHabit(newHabitLabel);
|
try {
|
||||||
setNewHabitLabel("");
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
await addHabit(newHabitLabel);
|
||||||
|
setNewHabitLabel("");
|
||||||
|
} catch (addError) {
|
||||||
|
setError(
|
||||||
|
addError instanceof Error ? addError.message : "Could not add habit.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveHabit = (habitId: string) => {
|
const handleRemoveHabit = async (habitId: string) => {
|
||||||
removeHabit(habitId);
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
await removeHabit(habitId);
|
||||||
|
} catch (removeError) {
|
||||||
|
setError(
|
||||||
|
removeError instanceof Error
|
||||||
|
? removeError.message
|
||||||
|
: "Could not remove habit.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -199,6 +249,8 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
if (mode === "manage") {
|
if (mode === "manage") {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Custom habits</CardTitle>
|
<CardTitle className="text-base">Custom habits</CardTitle>
|
||||||
@@ -213,15 +265,20 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
<Input
|
<Input
|
||||||
value={newHabitLabel}
|
value={newHabitLabel}
|
||||||
placeholder="Add a custom habit"
|
placeholder="Add a custom habit"
|
||||||
|
disabled={isSaving}
|
||||||
onChange={(event) => setNewHabitLabel(event.target.value)}
|
onChange={(event) => setNewHabitLabel(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleAddHabit();
|
void handleAddHabit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button type="button" onClick={handleAddHabit}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving || newHabitLabel.trim().length === 0}
|
||||||
|
onClick={() => void handleAddHabit()}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,6 +322,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
<DailyTaskList tasks={dailyTasks} onToggle={handleToggle} />
|
<DailyTaskList tasks={dailyTasks} onToggle={handleToggle} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
matchesFocusTopics,
|
matchesFocusTopics,
|
||||||
matchesQuizAnswers,
|
matchesQuizAnswers,
|
||||||
} from "@/lib/habit-catalog";
|
} from "@/lib/habit-catalog";
|
||||||
import { getHabits, saveHabits, type Habit } from "@/lib/habits-storage";
|
import {
|
||||||
|
ensureCatalogHabit,
|
||||||
|
getHabits,
|
||||||
|
type Habit,
|
||||||
|
} from "@/lib/habits-storage";
|
||||||
import {
|
import {
|
||||||
getProfilePreferences,
|
getProfilePreferences,
|
||||||
type ProfilePreferences,
|
type ProfilePreferences,
|
||||||
@@ -29,29 +33,6 @@ function getQuizAnswersForToday(): DailyQuizAnswers | null {
|
|||||||
return submission.answers;
|
return submission.answers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCatalogHabit(entryId: string, habits: Habit[]) {
|
|
||||||
const entry = getCatalogEntry(entryId);
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
return habits;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (habits.some((habit) => habit.id === entry.id)) {
|
|
||||||
return habits;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
...habits,
|
|
||||||
{
|
|
||||||
id: entry.id,
|
|
||||||
label: entry.label,
|
|
||||||
streak: 0,
|
|
||||||
lastCompletedDate: null,
|
|
||||||
isCustom: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCatalogTaskReason(
|
function getCatalogTaskReason(
|
||||||
entry: NonNullable<ReturnType<typeof getCatalogEntry>>,
|
entry: NonNullable<ReturnType<typeof getCatalogEntry>>,
|
||||||
preferences: ProfilePreferences,
|
preferences: ProfilePreferences,
|
||||||
@@ -71,34 +52,33 @@ function getCatalogTaskReason(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDailyTasks(): DailyTask[] {
|
export async function getDailyTasks(): Promise<DailyTask[]> {
|
||||||
let habits = getHabits();
|
|
||||||
const preferences = getProfilePreferences();
|
const preferences = getProfilePreferences();
|
||||||
const quizAnswers = getQuizAnswersForToday();
|
const quizAnswers = getQuizAnswersForToday();
|
||||||
const initialHabitCount = habits.length;
|
|
||||||
|
|
||||||
const selectedCatalogIds = habitCatalog
|
const selectedCatalogIds = habitCatalog
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const reason = getCatalogTaskReason(entry, preferences, quizAnswers);
|
const reason = getCatalogTaskReason(entry, preferences, quizAnswers);
|
||||||
return reason ? { id: entry.id, reason } : null;
|
return reason ? { catalogId: entry.id, reason } : null;
|
||||||
})
|
})
|
||||||
.filter((item): item is { id: string; reason: DailyTaskReason } => item !== null);
|
.filter(
|
||||||
|
(item): item is { catalogId: string; reason: DailyTaskReason } =>
|
||||||
|
item !== null,
|
||||||
|
);
|
||||||
|
|
||||||
for (const { id } of selectedCatalogIds) {
|
await Promise.all(
|
||||||
habits = ensureCatalogHabit(id, habits);
|
selectedCatalogIds.map(({ catalogId }) => ensureCatalogHabit(catalogId)),
|
||||||
}
|
);
|
||||||
|
|
||||||
if (habits.length !== initialHabitCount) {
|
const habits = await getHabits();
|
||||||
saveHabits(habits, { notify: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const customTasks: DailyTask[] = habits
|
const customTasks: DailyTask[] = habits
|
||||||
.filter((habit) => habit.isCustom)
|
.filter((habit) => habit.isCustom)
|
||||||
.map((habit) => ({ ...habit, reason: "custom" as const }));
|
.map((habit) => ({ ...habit, reason: "custom" as const }));
|
||||||
|
|
||||||
const catalogTasks: DailyTask[] = selectedCatalogIds
|
const catalogTasks: DailyTask[] = selectedCatalogIds
|
||||||
.map(({ id, reason }) => {
|
.map(({ catalogId, reason }) => {
|
||||||
const habit = habits.find((item) => item.id === id);
|
const habit = habits.find((item) => item.catalogId === catalogId);
|
||||||
return habit ? { ...habit, reason } : null;
|
return habit ? { ...habit, reason } : null;
|
||||||
})
|
})
|
||||||
.filter((task): task is DailyTask => task !== null);
|
.filter((task): task is DailyTask => task !== null);
|
||||||
@@ -106,6 +86,4 @@ export function getDailyTasks(): DailyTask[] {
|
|||||||
return [...customTasks, ...catalogTasks];
|
return [...customTasks, ...catalogTasks];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCatalogHabitId(habitId: string) {
|
export { isCatalogHabitId } from "@/lib/habits-storage";
|
||||||
return catalogHabitIds.has(habitId);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ export function getCatalogEntry(habitId: string) {
|
|||||||
return habitCatalog.find((habit) => habit.id === habitId);
|
return habitCatalog.find((habit) => habit.id === habitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCatalogIdByLabel(label: string) {
|
||||||
|
return habitCatalog.find((habit) => habit.label === label)?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCatalogLabel(label: string) {
|
||||||
|
return habitCatalog.some((habit) => habit.label === label);
|
||||||
|
}
|
||||||
|
|
||||||
export function matchesFocusTopics(
|
export function matchesFocusTopics(
|
||||||
entry: HabitCatalogEntry,
|
entry: HabitCatalogEntry,
|
||||||
preferences: ProfilePreferences,
|
preferences: ProfilePreferences,
|
||||||
|
|||||||
@@ -1,135 +1,314 @@
|
|||||||
import { catalogHabitIds, habitCatalog } from "@/lib/habit-catalog";
|
import {
|
||||||
|
catalogHabitIds,
|
||||||
|
getCatalogEntry,
|
||||||
|
getCatalogIdByLabel,
|
||||||
|
isCatalogLabel,
|
||||||
|
} from "@/lib/habit-catalog";
|
||||||
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
export type Habit = {
|
export type Habit = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
catalogId: string | null;
|
||||||
streak: number;
|
streak: number;
|
||||||
lastCompletedDate: string | null;
|
lastCompletedDate: string | null;
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "habit-pet-habits";
|
type HabitRow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultHabits: Habit[] = habitCatalog.map((entry) => ({
|
type HabitLogRow = {
|
||||||
id: entry.id,
|
habit_id: string;
|
||||||
label: entry.label,
|
completed_on: string;
|
||||||
streak: 0,
|
};
|
||||||
lastCompletedDate: null,
|
|
||||||
isCustom: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function getTodayDateKey(date = new Date()) {
|
export function getTodayDateKey(date = new Date()) {
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getYesterdayDateKey(date = new Date()) {
|
function calculateStreak(completedDates: string[], today = getTodayDateKey()) {
|
||||||
const yesterday = new Date(date);
|
if (completedDates.length === 0) {
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
return 0;
|
||||||
return getTodayDateKey(yesterday);
|
}
|
||||||
|
|
||||||
|
const dates = new Set(completedDates);
|
||||||
|
let streak = 0;
|
||||||
|
const cursor = new Date(`${today}T00:00:00`);
|
||||||
|
|
||||||
|
if (!dates.has(today)) {
|
||||||
|
cursor.setDate(cursor.getDate() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (dates.has(getTodayDateKey(cursor))) {
|
||||||
|
streak += 1;
|
||||||
|
cursor.setDate(cursor.getDate() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return streak;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHabit(habit: Partial<Habit> & { id: string; label: string }): Habit {
|
function getLastCompletedDate(completedDates: string[], today = getTodayDateKey()) {
|
||||||
|
if (completedDates.includes(today)) {
|
||||||
|
return today;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedDates = [...completedDates].sort().reverse();
|
||||||
|
return sortedDates[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapHabitRow(
|
||||||
|
row: HabitRow,
|
||||||
|
logsByHabitId: Map<string, string[]>,
|
||||||
|
today = getTodayDateKey(),
|
||||||
|
): Habit {
|
||||||
|
const completedDates = logsByHabitId.get(row.id) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: habit.id,
|
id: row.id,
|
||||||
label: habit.label,
|
label: row.name,
|
||||||
streak: habit.streak ?? 0,
|
catalogId: getCatalogIdByLabel(row.name),
|
||||||
lastCompletedDate: habit.lastCompletedDate ?? null,
|
streak: calculateStreak(completedDates, today),
|
||||||
isCustom: habit.isCustom ?? !catalogHabitIds.has(habit.id),
|
lastCompletedDate: getLastCompletedDate(completedDates, today),
|
||||||
|
isCustom: !isCatalogLabel(row.name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAuthenticatedUserId() {
|
||||||
|
const supabase = createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error,
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (error || !user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchHabitLogs(userId: string) {
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("habit_logs")
|
||||||
|
.select("habit_id, completed_on")
|
||||||
|
.eq("user_id", userId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsByHabitId = new Map<string, string[]>();
|
||||||
|
|
||||||
|
for (const log of (data ?? []) as HabitLogRow[]) {
|
||||||
|
const existing = logsByHabitId.get(log.habit_id) ?? [];
|
||||||
|
existing.push(log.completed_on);
|
||||||
|
logsByHabitId.set(log.habit_id, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return logsByHabitId;
|
||||||
|
}
|
||||||
|
|
||||||
export function isHabitCompletedToday(habit: Habit, today = getTodayDateKey()) {
|
export function isHabitCompletedToday(habit: Habit, today = getTodayDateKey()) {
|
||||||
return habit.lastCompletedDate === today;
|
return habit.lastCompletedDate === today;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHabits(): Habit[] {
|
export async function getHabits(): Promise<Habit[]> {
|
||||||
if (typeof window === "undefined") {
|
const userId = await getAuthenticatedUserId();
|
||||||
return defaultHabits;
|
|
||||||
|
if (!userId) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
const supabase = createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("habits")
|
||||||
|
.select("id, name")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.eq("active", true)
|
||||||
|
.order("created_at", { ascending: true });
|
||||||
|
|
||||||
if (!raw) {
|
if (error) {
|
||||||
return defaultHabits;
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const logsByHabitId = await fetchHabitLogs(userId);
|
||||||
const habits = (JSON.parse(raw) as Habit[]).map(normalizeHabit);
|
|
||||||
return habits.length > 0 ? habits : defaultHabits;
|
return ((data ?? []) as HabitRow[]).map((row) =>
|
||||||
} catch {
|
mapHabitRow(row, logsByHabitId),
|
||||||
return defaultHabits;
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveHabits(habits: Habit[], options?: { notify?: boolean }) {
|
export async function ensureCatalogHabit(catalogId: string): Promise<Habit | null> {
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(habits));
|
const entry = getCatalogEntry(catalogId);
|
||||||
|
|
||||||
if (options?.notify !== false) {
|
if (!entry) {
|
||||||
notifyHabitPetDataUpdated();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data: existing, error: existingError } = await supabase
|
||||||
|
.from("habits")
|
||||||
|
.select("id, name")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.eq("name", entry.label)
|
||||||
|
.eq("active", true)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existingError) {
|
||||||
|
throw new Error(existingError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = existing as HabitRow | null;
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
const { data: inserted, error: insertError } = await supabase
|
||||||
|
.from("habits")
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
name: entry.label,
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
.select("id, name")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
throw new Error(insertError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
row = inserted as HabitRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsByHabitId = await fetchHabitLogs(userId);
|
||||||
|
return mapHabitRow(row, logsByHabitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleHabitCompletion(habitId: string): Habit[] {
|
export async function toggleHabitCompletion(habitId: string): Promise<Habit[]> {
|
||||||
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
const today = getTodayDateKey();
|
const today = getTodayDateKey();
|
||||||
const yesterday = getYesterdayDateKey();
|
const { data: existingLog, error: existingLogError } = await supabase
|
||||||
const habits = getHabits().map((habit) => {
|
.from("habit_logs")
|
||||||
if (habit.id !== habitId) {
|
.select("id")
|
||||||
return habit;
|
.eq("user_id", userId)
|
||||||
|
.eq("habit_id", habitId)
|
||||||
|
.eq("completed_on", today)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existingLogError) {
|
||||||
|
throw new Error(existingLogError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingLog) {
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from("habit_logs")
|
||||||
|
.delete()
|
||||||
|
.eq("id", existingLog.id);
|
||||||
|
|
||||||
|
if (deleteError) {
|
||||||
|
throw new Error(deleteError.message);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const { error: insertError } = await supabase.from("habit_logs").insert({
|
||||||
|
user_id: userId,
|
||||||
|
habit_id: habitId,
|
||||||
|
completed_on: today,
|
||||||
|
});
|
||||||
|
|
||||||
if (isHabitCompletedToday(habit, today)) {
|
if (insertError) {
|
||||||
return {
|
throw new Error(insertError.message);
|
||||||
...habit,
|
|
||||||
lastCompletedDate: null,
|
|
||||||
streak: Math.max(0, habit.streak - 1),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nextStreak =
|
notifyHabitPetDataUpdated();
|
||||||
habit.lastCompletedDate === yesterday ? habit.streak + 1 : 1;
|
return getHabits();
|
||||||
|
|
||||||
return {
|
|
||||||
...habit,
|
|
||||||
lastCompletedDate: today,
|
|
||||||
streak: nextStreak,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
saveHabits(habits);
|
|
||||||
return habits;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addHabit(label: string): Habit[] {
|
export async function addHabit(label: string): Promise<Habit[]> {
|
||||||
const trimmedLabel = label.trim();
|
const trimmedLabel = label.trim();
|
||||||
|
|
||||||
if (!trimmedLabel) {
|
if (!trimmedLabel) {
|
||||||
return getHabits();
|
return getHabits();
|
||||||
}
|
}
|
||||||
|
|
||||||
const habits = [
|
const userId = await getAuthenticatedUserId();
|
||||||
...getHabits(),
|
|
||||||
normalizeHabit({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
label: trimmedLabel,
|
|
||||||
streak: 0,
|
|
||||||
lastCompletedDate: null,
|
|
||||||
isCustom: true,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
saveHabits(habits);
|
if (!userId) {
|
||||||
return habits;
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const { error } = await supabase.from("habits").insert({
|
||||||
|
user_id: userId,
|
||||||
|
name: trimmedLabel,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyHabitPetDataUpdated();
|
||||||
|
return getHabits();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeHabit(habitId: string): Habit[] {
|
export async function removeHabit(habitId: string): Promise<Habit[]> {
|
||||||
const habits = getHabits().filter(
|
const userId = await getAuthenticatedUserId();
|
||||||
(habit) => habit.id !== habitId || !habit.isCustom,
|
|
||||||
);
|
if (!userId) {
|
||||||
saveHabits(habits);
|
return [];
|
||||||
return habits;
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data: habit, error: habitError } = await supabase
|
||||||
|
.from("habits")
|
||||||
|
.select("id, name")
|
||||||
|
.eq("id", habitId)
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (habitError) {
|
||||||
|
throw new Error(habitError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!habit || isCatalogLabel(habit.name)) {
|
||||||
|
return getHabits();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from("habits")
|
||||||
|
.delete()
|
||||||
|
.eq("id", habitId)
|
||||||
|
.eq("user_id", userId);
|
||||||
|
|
||||||
|
if (deleteError) {
|
||||||
|
throw new Error(deleteError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyHabitPetDataUpdated();
|
||||||
|
return getHabits();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomHabits() {
|
export async function getCustomHabits() {
|
||||||
return getHabits().filter((habit) => habit.isCustom);
|
const habits = await getHabits();
|
||||||
|
return habits.filter((habit) => habit.isCustom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCatalogHabitId(habitId: string) {
|
||||||
|
return catalogHabitIds.has(habitId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user