daily quiz entries store in db correctly
This commit is contained in:
@@ -29,10 +29,14 @@ export function AvatarStatusCard() {
|
|||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkedIn = hasCompletedDailyQuizToday();
|
async function loadStatus() {
|
||||||
setHasCheckedInToday(checkedIn);
|
const checkedIn = await hasCompletedDailyQuizToday();
|
||||||
setCondition(getAvatarConditionForToday() ?? defaultAvatarCondition);
|
setHasCheckedInToday(checkedIn);
|
||||||
setIsReady(true);
|
setCondition((await getAvatarConditionForToday()) ?? defaultAvatarCondition);
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
|
|||||||
@@ -16,19 +16,18 @@ import {
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
defaultDailyQuizAnswers,
|
defaultDailyQuizAnswers,
|
||||||
|
formatSleepHours,
|
||||||
|
SLEEP_HOURS_MAX,
|
||||||
|
SLEEP_HOURS_MIN,
|
||||||
|
SLEEP_HOURS_STEP,
|
||||||
WELLNESS_SCALE_MAX,
|
WELLNESS_SCALE_MAX,
|
||||||
type DailyQuizAnswers,
|
type DailyQuizAnswers,
|
||||||
type DailyQuizSubmission,
|
type DailyQuizSubmission,
|
||||||
} from "@/lib/avatar-state";
|
} from "@/lib/avatar-state";
|
||||||
import {
|
import {
|
||||||
getDailyQuizSubmission,
|
getDailyEntryForToday,
|
||||||
getTodayDateKey,
|
saveDailyEntry,
|
||||||
saveDailyQuizSubmission,
|
|
||||||
} from "@/lib/daily-quiz-storage";
|
} from "@/lib/daily-quiz-storage";
|
||||||
import {
|
|
||||||
getJournalEntryForToday,
|
|
||||||
saveJournalEntry,
|
|
||||||
} from "@/lib/journal-storage";
|
|
||||||
|
|
||||||
export function DailyQuizForm() {
|
export function DailyQuizForm() {
|
||||||
const [answers, setAnswers] = useState<DailyQuizAnswers>(
|
const [answers, setAnswers] = useState<DailyQuizAnswers>(
|
||||||
@@ -44,19 +43,23 @@ export function DailyQuizForm() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadDailyCheckIn() {
|
async function loadDailyCheckIn() {
|
||||||
const existingSubmission = getDailyQuizSubmission();
|
try {
|
||||||
const existingJournal = await getJournalEntryForToday();
|
const existingEntry = await getDailyEntryForToday();
|
||||||
|
|
||||||
if (existingSubmission?.date === getTodayDateKey()) {
|
if (existingEntry) {
|
||||||
setSubmission(existingSubmission);
|
setSubmission(existingEntry);
|
||||||
setAnswers(existingSubmission.answers);
|
setAnswers(existingEntry.answers);
|
||||||
|
setJournal(existingEntry.journal);
|
||||||
|
}
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(
|
||||||
|
loadError instanceof Error
|
||||||
|
? loadError.message
|
||||||
|
: "Could not load today's check-in.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingJournal) {
|
|
||||||
setJournal(existingJournal.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsReady(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadDailyCheckIn();
|
void loadDailyCheckIn();
|
||||||
@@ -84,9 +87,8 @@ export function DailyQuizForm() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedSubmission = saveDailyQuizSubmission(answers);
|
const savedEntry = await saveDailyEntry(answers, journal);
|
||||||
await saveJournalEntry(journal);
|
setSubmission(savedEntry);
|
||||||
setSubmission(savedSubmission);
|
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
setError(
|
setError(
|
||||||
submitError instanceof Error
|
submitError instanceof Error
|
||||||
@@ -117,7 +119,7 @@ export function DailyQuizForm() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Wellness</CardTitle>
|
<CardTitle className="text-base">Wellness</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Sliders from 1 to 5 for how you're doing today.
|
Rate your wellness from 1 to 5. Sleep length uses hours.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-5">
|
<CardContent className="space-y-5">
|
||||||
@@ -146,6 +148,10 @@ export function DailyQuizForm() {
|
|||||||
id="sleep-length"
|
id="sleep-length"
|
||||||
label="How long did you sleep?"
|
label="How long did you sleep?"
|
||||||
value={answers.sleepLength}
|
value={answers.sleepLength}
|
||||||
|
min={SLEEP_HOURS_MIN}
|
||||||
|
max={SLEEP_HOURS_MAX}
|
||||||
|
step={SLEEP_HOURS_STEP}
|
||||||
|
formatValue={formatSleepHours}
|
||||||
disabled={isCompleted}
|
disabled={isCompleted}
|
||||||
onChange={(value) => updateAnswer("sleepLength", value)}
|
onChange={(value) => updateAnswer("sleepLength", value)}
|
||||||
/>
|
/>
|
||||||
@@ -162,9 +168,7 @@ export function DailyQuizForm() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Daily journal</CardTitle>
|
<CardTitle className="text-base">Daily journal</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Write about your day.</CardDescription>
|
||||||
Write about your day. Saved separately from your quiz answers.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<Label htmlFor="journal" className="sr-only">
|
<Label htmlFor="journal" className="sr-only">
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ export function DailyQuizLinkButton() {
|
|||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsCompleted(hasCompletedDailyQuizToday());
|
async function loadStatus() {
|
||||||
setIsReady(true);
|
setIsCompleted(await hasCompletedDailyQuizToday());
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!isReady || isCompleted) {
|
if (!isReady || isCompleted) {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
setDailyTasks(tasks);
|
setDailyTasks(tasks);
|
||||||
setCustomHabits(customs);
|
setCustomHabits(customs);
|
||||||
setFocusTopics(getProfilePreferences().focusTopics);
|
setFocusTopics(getProfilePreferences().focusTopics);
|
||||||
setQuizCompletedToday(hasCompletedDailyQuizToday());
|
setQuizCompletedToday(await hasCompletedDailyQuizToday());
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
setError(
|
setError(
|
||||||
|
|||||||
@@ -17,26 +17,49 @@ export type AvatarCondition = {
|
|||||||
export type DailyQuizSubmission = {
|
export type DailyQuizSubmission = {
|
||||||
date: string;
|
date: string;
|
||||||
answers: DailyQuizAnswers;
|
answers: DailyQuizAnswers;
|
||||||
|
journal: string;
|
||||||
condition: AvatarCondition;
|
condition: AvatarCondition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WELLNESS_SCALE_MAX = 5;
|
export const WELLNESS_SCALE_MAX = 5;
|
||||||
|
|
||||||
|
export const SLEEP_HOURS_MIN = 0.5;
|
||||||
|
export const SLEEP_HOURS_MAX = 12;
|
||||||
|
export const SLEEP_HOURS_STEP = 0.5;
|
||||||
|
export const DEFAULT_SLEEP_HOURS = 7.5;
|
||||||
|
|
||||||
export const defaultDailyQuizAnswers: DailyQuizAnswers = {
|
export const defaultDailyQuizAnswers: DailyQuizAnswers = {
|
||||||
feeling: 3,
|
feeling: 3,
|
||||||
stress: 3,
|
stress: 3,
|
||||||
energy: 3,
|
energy: 3,
|
||||||
sleepLength: 3,
|
sleepLength: DEFAULT_SLEEP_HOURS,
|
||||||
sleepQuality: 3,
|
sleepQuality: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function clampSleepHours(hours: number) {
|
||||||
|
const stepped = Math.round(hours / SLEEP_HOURS_STEP) * SLEEP_HOURS_STEP;
|
||||||
|
return Math.min(SLEEP_HOURS_MAX, Math.max(SLEEP_HOURS_MIN, stepped));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSleepHours(hours: number) {
|
||||||
|
return hours === 1 ? "1 hour" : `${hours} hours`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleepHoursToWellnessScore(hours: number) {
|
||||||
|
const score =
|
||||||
|
1 + ((clampSleepHours(hours) - SLEEP_HOURS_MIN) / (SLEEP_HOURS_MAX - SLEEP_HOURS_MIN)) * 4;
|
||||||
|
|
||||||
|
return clampStat(Math.round(score));
|
||||||
|
}
|
||||||
|
|
||||||
export function computeAvatarCondition(
|
export function computeAvatarCondition(
|
||||||
answers: DailyQuizAnswers,
|
answers: DailyQuizAnswers,
|
||||||
): AvatarCondition {
|
): AvatarCondition {
|
||||||
const { feeling, stress, energy, sleepLength, sleepQuality } = answers;
|
const { feeling, stress, energy, sleepLength, sleepQuality } = answers;
|
||||||
|
|
||||||
const wellnessAverage = (feeling + energy + (WELLNESS_SCALE_MAX + 1 - stress)) / 3;
|
const wellnessAverage = (feeling + energy + (WELLNESS_SCALE_MAX + 1 - stress)) / 3;
|
||||||
const sleepAverage = (sleepLength + sleepQuality) / 2;
|
const sleepScore = sleepHoursToWellnessScore(sleepLength);
|
||||||
|
const sleepAverage = (sleepScore + sleepQuality) / 2;
|
||||||
const health = Math.round((wellnessAverage + sleepAverage) / 2);
|
const health = Math.round((wellnessAverage + sleepAverage) / 2);
|
||||||
const energyLevel = Math.round((energy + sleepQuality) / 2);
|
const energyLevel = Math.round((energy + sleepQuality) / 2);
|
||||||
|
|
||||||
@@ -44,7 +67,7 @@ export function computeAvatarCondition(
|
|||||||
|
|
||||||
if (energyLevel >= 4 && feeling >= 4 && health >= 4) {
|
if (energyLevel >= 4 && feeling >= 4 && health >= 4) {
|
||||||
avatarMood = "happy";
|
avatarMood = "happy";
|
||||||
} else if (energy <= 2 || sleepLength <= 2 || sleepQuality <= 2) {
|
} else if (energy <= 2 || sleepLength < 6 || sleepQuality <= 2) {
|
||||||
avatarMood = "tired";
|
avatarMood = "tired";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +105,19 @@ export function normalizeDailyQuizAnswers(
|
|||||||
: value;
|
: value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeSleepLength = (value: number | undefined) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return defaultDailyQuizAnswers.sleepLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clampSleepHours(value);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
feeling: scaleValue(answers.feeling ?? answers.mood, defaultDailyQuizAnswers.feeling),
|
feeling: scaleValue(answers.feeling ?? answers.mood, defaultDailyQuizAnswers.feeling),
|
||||||
stress: scaleValue(answers.stress, defaultDailyQuizAnswers.stress),
|
stress: scaleValue(answers.stress, defaultDailyQuizAnswers.stress),
|
||||||
energy: scaleValue(answers.energy, defaultDailyQuizAnswers.energy),
|
energy: scaleValue(answers.energy, defaultDailyQuizAnswers.energy),
|
||||||
sleepLength: scaleValue(answers.sleepLength, defaultDailyQuizAnswers.sleepLength),
|
sleepLength: normalizeSleepLength(answers.sleepLength),
|
||||||
sleepQuality: scaleValue(
|
sleepQuality: scaleValue(
|
||||||
answers.sleepQuality,
|
answers.sleepQuality,
|
||||||
defaultDailyQuizAnswers.sleepQuality,
|
defaultDailyQuizAnswers.sleepQuality,
|
||||||
|
|||||||
@@ -6,69 +6,142 @@ import {
|
|||||||
type DailyQuizSubmission,
|
type DailyQuizSubmission,
|
||||||
} from "@/lib/avatar-state";
|
} from "@/lib/avatar-state";
|
||||||
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
||||||
const STORAGE_KEY = "habit-pet-daily-quiz";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
|
type DailyEntryRow = {
|
||||||
|
entry_date: string;
|
||||||
|
mood: number;
|
||||||
|
stress: number;
|
||||||
|
energy: number;
|
||||||
|
sleep_hours: number;
|
||||||
|
sleep_quality: number;
|
||||||
|
journal: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
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 normalizeSubmission(
|
function mapRowToSubmission(row: DailyEntryRow): DailyQuizSubmission {
|
||||||
submission: DailyQuizSubmission,
|
const answers = normalizeDailyQuizAnswers({
|
||||||
): DailyQuizSubmission {
|
feeling: row.mood,
|
||||||
const answers = normalizeDailyQuizAnswers(submission.answers);
|
stress: row.stress,
|
||||||
|
energy: row.energy,
|
||||||
|
sleepLength: Number(row.sleep_hours),
|
||||||
|
sleepQuality: row.sleep_quality,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...submission,
|
date: row.entry_date,
|
||||||
answers,
|
answers,
|
||||||
|
journal: row.journal ?? "",
|
||||||
condition: computeAvatarCondition(answers),
|
condition: computeAvatarCondition(answers),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDailyQuizSubmission(): DailyQuizSubmission | null {
|
async function getAuthenticatedUserId() {
|
||||||
if (typeof window === "undefined") {
|
const supabase = createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error,
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (error || !user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
return user.id;
|
||||||
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return normalizeSubmission(JSON.parse(raw) as DailyQuizSubmission);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasCompletedDailyQuizToday() {
|
export async function getDailyEntryForToday(): Promise<DailyQuizSubmission | null> {
|
||||||
const submission = getDailyQuizSubmission();
|
const userId = await getAuthenticatedUserId();
|
||||||
return submission?.date === getTodayDateKey();
|
|
||||||
|
if (!userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const entryDate = getTodayDateKey();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("daily_entries")
|
||||||
|
.select(
|
||||||
|
"entry_date, mood, stress, energy, sleep_hours, sleep_quality, journal",
|
||||||
|
)
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.eq("entry_date", entryDate)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapRowToSubmission(data as DailyEntryRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveDailyQuizSubmission(
|
export async function getDailyQuizSubmission() {
|
||||||
|
return getDailyEntryForToday();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasCompletedDailyQuizToday() {
|
||||||
|
const entry = await getDailyEntryForToday();
|
||||||
|
return entry !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDailyEntry(
|
||||||
answers: DailyQuizAnswers,
|
answers: DailyQuizAnswers,
|
||||||
): DailyQuizSubmission {
|
journal: string,
|
||||||
const submission: DailyQuizSubmission = {
|
): Promise<DailyQuizSubmission> {
|
||||||
date: getTodayDateKey(),
|
const userId = await getAuthenticatedUserId();
|
||||||
answers,
|
|
||||||
condition: computeAvatarCondition(answers),
|
|
||||||
};
|
|
||||||
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(submission));
|
if (!userId) {
|
||||||
|
throw new Error("You must be signed in to save your daily check-in.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const entryDate = getTodayDateKey();
|
||||||
|
const normalizedAnswers = normalizeDailyQuizAnswers(answers);
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("daily_entries")
|
||||||
|
.upsert(
|
||||||
|
{
|
||||||
|
user_id: userId,
|
||||||
|
entry_date: entryDate,
|
||||||
|
mood: normalizedAnswers.feeling,
|
||||||
|
stress: normalizedAnswers.stress,
|
||||||
|
energy: normalizedAnswers.energy,
|
||||||
|
sleep_hours: normalizedAnswers.sleepLength,
|
||||||
|
sleep_quality: normalizedAnswers.sleepQuality,
|
||||||
|
journal,
|
||||||
|
},
|
||||||
|
{ onConflict: "user_id,entry_date" },
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
"entry_date, mood, stress, energy, sleep_hours, sleep_quality, journal",
|
||||||
|
)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submission = mapRowToSubmission(data as DailyEntryRow);
|
||||||
notifyHabitPetDataUpdated();
|
notifyHabitPetDataUpdated();
|
||||||
|
|
||||||
return submission;
|
return submission;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvatarConditionForToday(): AvatarCondition | null {
|
export async function saveDailyQuizSubmission(
|
||||||
const submission = getDailyQuizSubmission();
|
answers: DailyQuizAnswers,
|
||||||
|
journal = "",
|
||||||
if (!submission || submission.date !== getTodayDateKey()) {
|
) {
|
||||||
return null;
|
return saveDailyEntry(answers, journal);
|
||||||
}
|
}
|
||||||
|
|
||||||
return submission.condition;
|
export async function getAvatarConditionForToday(): Promise<AvatarCondition | null> {
|
||||||
|
const entry = await getDailyEntryForToday();
|
||||||
|
return entry?.condition ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DailyQuizAnswers } from "@/lib/avatar-state";
|
import type { DailyQuizAnswers } from "@/lib/avatar-state";
|
||||||
import { getDailyQuizSubmission, getTodayDateKey } from "@/lib/daily-quiz-storage";
|
import { getDailyEntryForToday } from "@/lib/daily-quiz-storage";
|
||||||
import {
|
import {
|
||||||
catalogHabitIds,
|
catalogHabitIds,
|
||||||
getCatalogEntry,
|
getCatalogEntry,
|
||||||
@@ -23,14 +23,9 @@ export type DailyTask = Habit & {
|
|||||||
reason: DailyTaskReason;
|
reason: DailyTaskReason;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getQuizAnswersForToday(): DailyQuizAnswers | null {
|
async function getQuizAnswersForToday(): Promise<DailyQuizAnswers | null> {
|
||||||
const submission = getDailyQuizSubmission();
|
const entry = await getDailyEntryForToday();
|
||||||
|
return entry?.answers ?? null;
|
||||||
if (!submission || submission.date !== getTodayDateKey()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return submission.answers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCatalogTaskReason(
|
function getCatalogTaskReason(
|
||||||
@@ -54,7 +49,7 @@ function getCatalogTaskReason(
|
|||||||
|
|
||||||
export async function getDailyTasks(): Promise<DailyTask[]> {
|
export async function getDailyTasks(): Promise<DailyTask[]> {
|
||||||
const preferences = getProfilePreferences();
|
const preferences = getProfilePreferences();
|
||||||
const quizAnswers = getQuizAnswersForToday();
|
const quizAnswers = await getQuizAnswersForToday();
|
||||||
|
|
||||||
const selectedCatalogIds = habitCatalog
|
const selectedCatalogIds = habitCatalog
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const habitCatalog: HabitCatalogEntry[] = [
|
|||||||
label: "Wind down before bed",
|
label: "Wind down before bed",
|
||||||
focusTopics: ["Sleep"],
|
focusTopics: ["Sleep"],
|
||||||
suggestedByQuiz: (answers) =>
|
suggestedByQuiz: (answers) =>
|
||||||
answers.sleepLength <= 2 || answers.sleepQuality <= 2,
|
answers.sleepLength < 7 || answers.sleepQuality <= 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { createClient } from "@/lib/supabase/client";
|
|
||||||
|
|
||||||
export type JournalEntry = {
|
|
||||||
entryDate: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "habit-pet-journal";
|
|
||||||
|
|
||||||
export function getTodayDateKey(date = new Date()) {
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocalJournalEntry(entryDate = getTodayDateKey()): JournalEntry | null {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
||||||
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entry = JSON.parse(raw) as JournalEntry;
|
|
||||||
return entry.entryDate === entryDate ? entry : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLocalJournalEntry(content: string, entryDate = getTodayDateKey()) {
|
|
||||||
const entry: JournalEntry = { entryDate, content };
|
|
||||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(entry));
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getJournalEntryForToday(): Promise<JournalEntry | null> {
|
|
||||||
const entryDate = getTodayDateKey();
|
|
||||||
const supabase = createClient();
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return getLocalJournalEntry(entryDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("journal_entries")
|
|
||||||
.select("entry_date, content")
|
|
||||||
.eq("user_id", user.id)
|
|
||||||
.eq("entry_date", entryDate)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Failed to load journal entry:", error.message);
|
|
||||||
return getLocalJournalEntry(entryDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
entryDate: data.entry_date,
|
|
||||||
content: data.content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveJournalEntry(content: string): Promise<JournalEntry> {
|
|
||||||
const entryDate = getTodayDateKey();
|
|
||||||
const supabase = createClient();
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return saveLocalJournalEntry(content, entryDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("journal_entries")
|
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
user_id: user.id,
|
|
||||||
entry_date: entryDate,
|
|
||||||
content,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{ onConflict: "user_id,entry_date" },
|
|
||||||
)
|
|
||||||
.select("entry_date, content")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Failed to save journal entry:", error.message);
|
|
||||||
return saveLocalJournalEntry(content, entryDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
entryDate: data.entry_date,
|
|
||||||
content: data.content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasJournalEntryToday(entry: JournalEntry | null) {
|
|
||||||
return entry?.entryDate === getTodayDateKey() && entry.content.trim().length > 0;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
create table if not exists public.journal_entries (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
user_id uuid not null references auth.users (id) on delete cascade,
|
|
||||||
entry_date date not null,
|
|
||||||
content text not null default '',
|
|
||||||
created_at timestamptz not null default now(),
|
|
||||||
updated_at timestamptz not null default now(),
|
|
||||||
unique (user_id, entry_date)
|
|
||||||
);
|
|
||||||
|
|
||||||
create index if not exists journal_entries_user_id_entry_date_idx
|
|
||||||
on public.journal_entries (user_id, entry_date desc);
|
|
||||||
|
|
||||||
alter table public.journal_entries enable row level security;
|
|
||||||
|
|
||||||
create policy "Users can read own journal entries"
|
|
||||||
on public.journal_entries
|
|
||||||
for select
|
|
||||||
using (auth.uid() = user_id);
|
|
||||||
|
|
||||||
create policy "Users can insert own journal entries"
|
|
||||||
on public.journal_entries
|
|
||||||
for insert
|
|
||||||
with check (auth.uid() = user_id);
|
|
||||||
|
|
||||||
create policy "Users can update own journal entries"
|
|
||||||
on public.journal_entries
|
|
||||||
for update
|
|
||||||
using (auth.uid() = user_id)
|
|
||||||
with check (auth.uid() = user_id);
|
|
||||||
|
|
||||||
create policy "Users can delete own journal entries"
|
|
||||||
on public.journal_entries
|
|
||||||
for delete
|
|
||||||
using (auth.uid() = user_id);
|
|
||||||
Reference in New Issue
Block a user