checking other branch
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
110
lib/journal-storage.ts
Normal 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;
|
||||||
|
}
|
||||||
35
supabase/migrations/20260523120000_journal_entries.sql
Normal file
35
supabase/migrations/20260523120000_journal_entries.sql
Normal 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);
|
||||||
Reference in New Issue
Block a user