checking other branch

This commit is contained in:
masongga
2026-05-23 18:59:27 -06:00
parent 94dbd18946
commit 7deb3e83cb
4 changed files with 193 additions and 16 deletions

View File

@@ -25,29 +25,45 @@ import {
getTodayDateKey, getTodayDateKey,
saveDailyQuizSubmission, 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>(
defaultDailyQuizAnswers, defaultDailyQuizAnswers,
); );
const [journal, setJournal] = useState("");
const [submission, setSubmission] = useState<DailyQuizSubmission | null>( const [submission, setSubmission] = useState<DailyQuizSubmission | null>(
null, null,
); );
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
useEffect(() => { useEffect(() => {
const existingSubmission = getDailyQuizSubmission(); async function loadDailyCheckIn() {
const existingSubmission = getDailyQuizSubmission();
const existingJournal = await getJournalEntryForToday();
if (existingSubmission?.date === getTodayDateKey()) { if (existingSubmission?.date === getTodayDateKey()) {
setSubmission(existingSubmission); setSubmission(existingSubmission);
setAnswers(existingSubmission.answers); setAnswers(existingSubmission.answers);
}
if (existingJournal) {
setJournal(existingJournal.content);
}
setIsReady(true);
} }
setIsReady(true); void loadDailyCheckIn();
}, []); }, []);
const isCompleted = submission !== null; const isCompleted = submission !== null;
const canSubmit = !isCompleted; const canSubmit = !isCompleted && !isSubmitting;
const updateAnswer = <K extends keyof DailyQuizAnswers>( const updateAnswer = <K extends keyof DailyQuizAnswers>(
key: K, key: K,
@@ -59,13 +75,27 @@ export function DailyQuizForm() {
})); }));
}; };
const handleSubmit = () => { const handleSubmit = async () => {
if (!canSubmit) { if (!canSubmit) {
return; return;
} }
const savedSubmission = saveDailyQuizSubmission(answers); setIsSubmitting(true);
setSubmission(savedSubmission); setError(null);
try {
const savedSubmission = saveDailyQuizSubmission(answers);
await saveJournalEntry(journal);
setSubmission(savedSubmission);
} catch (submitError) {
setError(
submitError instanceof Error
? submitError.message
: "Could not save your daily check-in.",
);
} finally {
setIsSubmitting(false);
}
}; };
if (!isReady) { if (!isReady) {
@@ -132,7 +162,9 @@ export function DailyQuizForm() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Daily journal</CardTitle> <CardTitle className="text-base">Daily journal</CardTitle>
<CardDescription>Write about your day.</CardDescription> <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">
@@ -140,15 +172,17 @@ export function DailyQuizForm() {
</Label> </Label>
<textarea <textarea
id="journal" id="journal"
value={answers.journal} value={journal}
disabled={isCompleted} disabled={isCompleted}
onChange={(event) => updateAnswer("journal", event.target.value)} onChange={(event) => setJournal(event.target.value)}
className="min-h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60" className="min-h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60"
placeholder="Write about your day." placeholder="Write about your day."
/> />
</CardContent> </CardContent>
</Card> </Card>
{error ? <p className="text-sm text-red-500">{error}</p> : null}
{isCompleted && submission ? ( {isCompleted && submission ? (
<Card> <Card>
<CardContent className="flex flex-col items-center gap-4 pt-6"> <CardContent className="flex flex-col items-center gap-4 pt-6">
@@ -177,7 +211,7 @@ export function DailyQuizForm() {
</Card> </Card>
) : ( ) : (
<Button className="w-full" disabled={!canSubmit} onClick={handleSubmit}> <Button className="w-full" disabled={!canSubmit} onClick={handleSubmit}>
Complete daily quiz {isSubmitting ? "Saving..." : "Complete daily quiz"}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -6,7 +6,6 @@ export type DailyQuizAnswers = {
energy: number; energy: number;
sleepLength: number; sleepLength: number;
sleepQuality: number; sleepQuality: number;
journal: string;
}; };
export type AvatarCondition = { export type AvatarCondition = {
@@ -29,7 +28,6 @@ export const defaultDailyQuizAnswers: DailyQuizAnswers = {
energy: 3, energy: 3,
sleepLength: 3, sleepLength: 3,
sleepQuality: 3, sleepQuality: 3,
journal: "",
}; };
export function computeAvatarCondition( export function computeAvatarCondition(
@@ -71,6 +69,7 @@ export const defaultAvatarCondition: AvatarCondition = {
export function normalizeDailyQuizAnswers( export function normalizeDailyQuizAnswers(
answers: Partial<DailyQuizAnswers> & { answers: Partial<DailyQuizAnswers> & {
mood?: number; mood?: number;
journal?: string;
}, },
): DailyQuizAnswers { ): DailyQuizAnswers {
const scaleValue = (value: number | undefined, fallback: number) => { const scaleValue = (value: number | undefined, fallback: number) => {
@@ -92,6 +91,5 @@ export function normalizeDailyQuizAnswers(
answers.sleepQuality, answers.sleepQuality,
defaultDailyQuizAnswers.sleepQuality, defaultDailyQuizAnswers.sleepQuality,
), ),
journal: answers.journal ?? "",
}; };
} }

110
lib/journal-storage.ts Normal file
View File

@@ -0,0 +1,110 @@
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;
}

View File

@@ -0,0 +1,35 @@
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);