From e41fc10c77c1f00f915bc1c530ef09855f17381e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 24 May 2026 10:09:50 -0600 Subject: [PATCH 1/2] Rename shop items to plain sprite labels and improve thumbnail visibility. Use code-side display names in the shop UI so labels stay correct even when DB rows lag, and add a neutral thumbnail glow so dark colors remain visible. --- app/globals.css | 13 +++++ components/shop/shop-content.tsx | 47 +++++++++++-------- components/shop/shop-item-preview-modal.tsx | 4 +- lib/character/presets.ts | 38 +++++++-------- lib/room-backgrounds.ts | 8 ++-- lib/shop-catalog.ts | 16 ++++++- ...00_update_shop_and_style_display_names.sql | 30 ++++++++++++ supabase/seed.sql | 46 +++++------------- 8 files changed, 122 insertions(+), 80 deletions(-) create mode 100644 supabase/migrations/20260525210000_update_shop_and_style_display_names.sql diff --git a/app/globals.css b/app/globals.css index ea8a992..c663222 100644 --- a/app/globals.css +++ b/app/globals.css @@ -262,6 +262,19 @@ @apply border-2 border-border bg-card px-2 py-1 font-pixel leading-snug text-card-foreground; box-shadow: var(--retro-shadow-sm); } + + .shop-item-thumbnail { + background: hsl(var(--muted) / 0.22); + } + + .shop-item-thumbnail-glow { + background: radial-gradient( + circle at 50% 58%, + hsl(52 55% 90% / 0.55) 0%, + hsl(48 30% 88% / 0.18) 45%, + transparent 72% + ); + } } @keyframes emotion-pop { diff --git a/components/shop/shop-content.tsx b/components/shop/shop-content.tsx index ba143c2..e304949 100644 --- a/components/shop/shop-content.tsx +++ b/components/shop/shop-content.tsx @@ -22,6 +22,7 @@ import { } from "@/lib/shop-storage"; import { getLayerLabel, + getShopItemDisplayName, type ShopItemRecord, type ShopLayerId, } from "@/lib/shop-catalog"; @@ -103,6 +104,8 @@ function ShopItemCard({ ? null : getShopItemThumbnailColors(item, characterColors); + const displayName = getShopItemDisplayName(item); + const actionLabel = (() => { if (isPending) { return owned ? "Updating..." : "Processing..."; @@ -123,27 +126,33 @@ function ShopItemCard({
-
- {room ? ( - // eslint-disable-next-line @next/next/no-img-element - - ) : thumbnailColors ? ( - +
+ {thumbnailColors || room ? ( +
) : null} +
+ {room ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : thumbnailColors ? ( + + ) : null} +

- {item.name} + {displayName}

Preview on your bit

-

{item.name}

+

{getShopItemDisplayName(item)}

{hasCheckedInToday ? completedTaskCount > 0 - ? `Your bit reacts to today's quiz and ${completedTaskCount} completed task${completedTaskCount === 1 ? "" : "s"}. Meals, hydration, sleep, and relaxing habits boost health and mood.` + ? `Your bit reacts to today's check-in and ${completedTaskCount} completed task${completedTaskCount === 1 ? "" : "s"}. Meals, hydration, sleep, and relaxing habits boost health and mood.` : "Your bit is reacting to today's wellness check-in. Complete habits to boost mood, energy, and health." : completedTaskCount > 0 - ? "Complete today's quiz for a full wellness read, then keep completing habits to boost your bit." - : "Complete today's quiz to update your bit's mood, energy, and health."} + ? "Complete today's check-in for a full wellness read, then keep completing habits to boost your bit." + : "Complete today's check-in to update your bit's mood, energy, and health."}

{!hasCheckedInToday ? ( ) : null} diff --git a/components/daily-quiz/daily-quiz-form.tsx b/components/daily-quiz/daily-quiz-form.tsx index b09adca..b5012d3 100644 --- a/components/daily-quiz/daily-quiz-form.tsx +++ b/components/daily-quiz/daily-quiz-form.tsx @@ -138,7 +138,7 @@ export function DailyQuizForm() { if (!isReady) { return ( -

Loading today's quiz...

+

Loading today's check-in...

); } @@ -155,7 +155,7 @@ export function DailyQuizForm() {

- Today's quiz is complete + Today's check-in is complete

Your answers are saved below. Come back tomorrow for your next @@ -264,7 +264,7 @@ export function DailyQuizForm() { {!isCompleted ? ( ) : null}

diff --git a/components/daily-quiz/daily-quiz-link-button.tsx b/components/daily-quiz/daily-quiz-link-button.tsx index ab09384..a815a13 100644 --- a/components/daily-quiz/daily-quiz-link-button.tsx +++ b/components/daily-quiz/daily-quiz-link-button.tsx @@ -23,14 +23,14 @@ export function DailyQuizLinkButton() { if (!isReady || isCompleted) { return ( ); } return ( ); } diff --git a/components/habits/habit-tracker.tsx b/components/habits/habit-tracker.tsx index 46e92dd..eeb2836 100644 --- a/components/habits/habit-tracker.tsx +++ b/components/habits/habit-tracker.tsx @@ -57,7 +57,7 @@ function DailyTaskList({ if (tasks.length === 0) { return (

- No daily tasks yet. Complete your daily quiz or update your profile so + No daily tasks yet. Complete your daily check-in or update your profile so AI can suggest habits for you.

); @@ -428,7 +428,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { Today's tasks Personalized from your focus topics - {quizCompletedToday ? ", today's quiz," : ""} and custom habits. + {quizCompletedToday ? ", today's check-in," : ""} and custom habits. @@ -485,17 +485,17 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { How daily tasks are picked Your tasks combine focus preferences, custom habits, and today's - quiz. + check-in.

Focus topics: {focusTopic ?? "None selected"}

- Daily quiz today:{" "} + Daily check-in today:{" "} {quizCompletedToday ? "Completed" : "Not completed yet"}

- Suggested habits can appear when your quiz shows low energy, high + Suggested habits can appear when your check-in shows low energy, high stress, or poor sleep.

@@ -548,7 +548,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { How daily tasks are picked Your tasks combine custom habits and AI suggestions based on your - profile, onboarding, daily quiz, and journal. + profile, onboarding, daily check-in, and journal. @@ -557,7 +557,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { {focusTopic ?? "None selected"}

- Daily quiz today:{" "} + Daily check-in today:{" "} {quizCompletedToday ? "Completed" : "Not completed yet"}

@@ -575,7 +575,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { Today's tasks - Personalized from your profile, daily quiz, journal, and custom habits. + Personalized from your profile, daily check-in, journal, and custom habits. diff --git a/components/journal/journal-calendar.tsx b/components/journal/journal-calendar.tsx new file mode 100644 index 0000000..52d1fb1 --- /dev/null +++ b/components/journal/journal-calendar.tsx @@ -0,0 +1,329 @@ +"use client"; + +import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { JournalEntryModal } from "@/components/journal/journal-entry-modal"; +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 type { DailyQuizSubmission } from "@/lib/avatar-state"; +import { + getDailyEntryByDate, + getDailyEntrySummariesForMonth, + getTodayDateKey, + type DailyEntrySummary, +} from "@/lib/daily-quiz-storage"; +import { cn } from "@/lib/utils"; + +const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +type CalendarCell = { + dateKey: string; + day: number; + inMonth: boolean; +}; + +function buildMonthGrid(year: number, month: number): CalendarCell[] { + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const grid: CalendarCell[] = []; + + for (let index = 0; index < firstDay.getDay(); index += 1) { + const date = new Date(year, month, index - firstDay.getDay() + 1); + grid.push({ + dateKey: date.toLocaleDateString("en-CA"), + day: date.getDate(), + inMonth: false, + }); + } + + for (let day = 1; day <= lastDay.getDate(); day += 1) { + const date = new Date(year, month, day); + grid.push({ + dateKey: date.toLocaleDateString("en-CA"), + day, + inMonth: true, + }); + } + + let trailingDay = 1; + while (grid.length % 7 !== 0) { + const date = new Date(year, month + 1, trailingDay); + grid.push({ + dateKey: date.toLocaleDateString("en-CA"), + day: date.getDate(), + inMonth: false, + }); + trailingDay += 1; + } + + return grid; +} + +function formatMonthLabel(year: number, month: number) { + return new Date(year, month, 1).toLocaleDateString(undefined, { + month: "long", + year: "numeric", + }); +} + +export function JournalCalendar() { + const todayKey = getTodayDateKey(); + const todayDate = new Date(); + const [visibleYear, setVisibleYear] = useState(todayDate.getFullYear()); + const [visibleMonth, setVisibleMonth] = useState(todayDate.getMonth()); + const [summaries, setSummaries] = useState([]); + const [selectedDateKey, setSelectedDateKey] = useState(null); + const [selectedEntry, setSelectedEntry] = useState( + null, + ); + const [isExpanded, setIsExpanded] = useState(false); + const [isLoadingMonth, setIsLoadingMonth] = useState(false); + const [isLoadingEntry, setIsLoadingEntry] = useState(false); + const [error, setError] = useState(null); + + const summaryByDate = useMemo( + () => new Map(summaries.map((summary) => [summary.date, summary])), + [summaries], + ); + + const monthGrid = useMemo( + () => buildMonthGrid(visibleYear, visibleMonth), + [visibleYear, visibleMonth], + ); + + const loadMonth = useCallback(async () => { + setIsLoadingMonth(true); + setError(null); + + try { + setSummaries( + await getDailyEntrySummariesForMonth(visibleYear, visibleMonth), + ); + } catch (loadError) { + setError( + loadError instanceof Error + ? loadError.message + : "Could not load journal history.", + ); + } finally { + setIsLoadingMonth(false); + } + }, [visibleMonth, visibleYear]); + + useEffect(() => { + if (!isExpanded) { + return; + } + + void loadMonth(); + }, [isExpanded, loadMonth]); + + useEffect(() => { + if (!isExpanded) { + return; + } + + const handleDataUpdated = () => { + void loadMonth(); + }; + + window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated); + return () => { + window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated); + }; + }, [isExpanded, loadMonth]); + + const goToPreviousMonth = () => { + if (visibleMonth === 0) { + setVisibleYear((current) => current - 1); + setVisibleMonth(11); + return; + } + + setVisibleMonth((current) => current - 1); + }; + + const goToNextMonth = () => { + if (visibleMonth === 11) { + setVisibleYear((current) => current + 1); + setVisibleMonth(0); + return; + } + + setVisibleMonth((current) => current + 1); + }; + + const handleDaySelect = async (dateKey: string) => { + const summary = summaryByDate.get(dateKey); + + if (!summary) { + return; + } + + setSelectedDateKey(dateKey); + setIsLoadingEntry(true); + setError(null); + + try { + setSelectedEntry(await getDailyEntryByDate(dateKey)); + } catch (loadError) { + setSelectedDateKey(null); + setError( + loadError instanceof Error + ? loadError.message + : "Could not load that journal entry.", + ); + } finally { + setIsLoadingEntry(false); + } + }; + + const closeEntryModal = () => { + setSelectedEntry(null); + setSelectedDateKey(null); + }; + + return ( + <> + + +

+ + +
+ + {isExpanded ? ( + +
+

+ {formatMonthLabel(visibleYear, visibleMonth)} +

+
+ + +
+
+
+ {WEEKDAY_LABELS.map((label) => ( +
+ {label} +
+ ))} +
+ +
+ {monthGrid.map((cell) => { + const summary = summaryByDate.get(cell.dateKey); + const hasEntry = Boolean(summary); + const isToday = cell.dateKey === todayKey; + const isSelected = cell.dateKey === selectedDateKey; + + return ( + + ); + })} +
+ +
+ + + Journal written + + + + Check-in only + +
+ + {isLoadingMonth ? ( +

Loading calendar...

+ ) : null} + {error ?

{error}

: null} +
+ ) : null} + + + + + ); +} diff --git a/components/journal/journal-entry-modal.tsx b/components/journal/journal-entry-modal.tsx new file mode 100644 index 0000000..27226b6 --- /dev/null +++ b/components/journal/journal-entry-modal.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { X } from "lucide-react"; +import { useEffect } from "react"; + +import { WellnessSlider } from "@/components/daily-quiz/wellness-slider"; +import { Button } from "@/components/ui/button"; +import { + formatSleepHours, + SLEEP_HOURS_MAX, + SLEEP_HOURS_MIN, + SLEEP_HOURS_STEP, + type DailyQuizSubmission, +} from "@/lib/avatar-state"; + +type JournalEntryModalProps = { + entry: DailyQuizSubmission | null; + onClose: () => void; +}; + +function formatEntryDateLabel(dateKey: string) { + const [year, month, day] = dateKey.split("-").map(Number); + const date = new Date(year, month - 1, day); + + return date.toLocaleDateString(undefined, { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }); +} + +export function JournalEntryModal({ entry, onClose }: JournalEntryModalProps) { + useEffect(() => { + if (!entry) { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [entry, onClose]); + + if (!entry) { + return null; + } + + const journalText = entry.journal.trim(); + + return ( +
+
event.stopPropagation()} + > +
+
+

+ Journal entry +

+

+ {formatEntryDateLabel(entry.date)} +

+
+ +
+ +
+
+

Daily journal

+

+ {journalText || "No journal written."} +

+
+ +
+

Wellness

+ undefined} + /> + undefined} + /> + undefined} + /> + undefined} + /> + undefined} + /> +
+
+
+
+ ); +} diff --git a/components/profile/profile-preferences-form.tsx b/components/profile/profile-preferences-form.tsx index f1b8b81..3fba700 100644 --- a/components/profile/profile-preferences-form.tsx +++ b/components/profile/profile-preferences-form.tsx @@ -247,13 +247,13 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) { try { await resetTodaysDailyQuiz(); setQuizResetMessage( - "Today's quiz was reset. Open the Quiz tab to take it again.", + "Today's check-in was reset. Open the Bit page to complete it again.", ); } catch (resetError) { setError( resetError instanceof Error ? resetError.message - : "Could not reset today's quiz.", + : "Could not reset today's check-in.", ); } finally { setIsResettingQuiz(false); @@ -337,7 +337,7 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) { Notification preferences - Control reminders for your daily quiz and habits. + Control reminders for your daily check-in and habits. @@ -425,7 +425,7 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {

- Reset clears today's wellness quiz entry so you can complete it + Reset clears today's wellness check-in entry so you can complete it again immediately. Demo points are for testing the shop only.

{quizResetMessage ? (

{quizResetMessage}

diff --git a/lib/daily-quiz-storage.ts b/lib/daily-quiz-storage.ts index f4eac1c..28f4a79 100644 --- a/lib/daily-quiz-storage.ts +++ b/lib/daily-quiz-storage.ts @@ -48,7 +48,7 @@ export async function resetTodaysDailyQuiz() { const userId = await getAuthenticatedUserId(); if (!userId) { - throw new Error("You must be signed in to reset today's quiz."); + throw new Error("You must be signed in to reset today's check-in."); } const supabase = createClient(); @@ -197,6 +197,89 @@ export async function saveDailyQuizSubmission( return saveDailyEntry(answers, journal); } +export type DailyEntrySummary = { + date: string; + hasJournal: boolean; +}; + +const DAILY_ENTRY_SELECT = + "entry_date, mood, stress, energy, sleep_hours, sleep_quality, journal"; + +function formatDateKey(date: Date) { + return date.toLocaleDateString("en-CA"); +} + +function getMonthDateBounds(year: number, month: number) { + const monthStart = new Date(year, month, 1); + const monthEnd = new Date(year, month + 1, 0); + + return { + startKey: formatDateKey(monthStart), + endKey: formatDateKey(monthEnd), + }; +} + +export async function getDailyEntrySummariesForMonth( + year: number, + month: number, +): Promise { + const userId = await getAuthenticatedUserId(); + + if (!userId) { + return []; + } + + const { startKey, endKey } = getMonthDateBounds(year, month); + const supabase = createClient(); + const { data, error } = await supabase + .from("daily_entries") + .select("entry_date, journal") + .eq("user_id", userId) + .gte("entry_date", startKey) + .lte("entry_date", endKey) + .order("entry_date", { ascending: true }); + + if (error) { + throw new Error(error.message); + } + + return (data ?? []).map((row) => ({ + date: row.entry_date as string, + hasJournal: Boolean((row.journal as string | null)?.trim()), + })); +} + +export async function getDailyEntryByDate( + dateKey: string, +): Promise { + const userId = await getAuthenticatedUserId(); + + if (!userId) { + return null; + } + + const supabase = createClient(); + const { data, error } = await supabase + .from("daily_entries") + .select(DAILY_ENTRY_SELECT) + .eq("user_id", userId) + .eq("entry_date", dateKey) + .order("created_at", { ascending: false }) + .limit(1); + + if (error) { + throw new Error(error.message); + } + + const row = data?.[0]; + + if (!row) { + return null; + } + + return mapRowToSubmission(row as DailyEntryRow); +} + export async function getAvatarConditionForToday(): Promise { const [entry, completedHabitLabels] = await Promise.all([ getDailyEntryForToday(), diff --git a/lib/daily-reminders.ts b/lib/daily-reminders.ts index 761a2d4..ee676d8 100644 --- a/lib/daily-reminders.ts +++ b/lib/daily-reminders.ts @@ -121,7 +121,7 @@ export function buildDailyReminderNotification(status: DailyReminderStatus) { if (!status.quizCompleted && status.incompleteTaskCount > 0) { return { title: "Time for your daily check-in", - body: `Complete today's wellness quiz and ${status.incompleteTaskCount} remaining habit${status.incompleteTaskCount === 1 ? "" : "s"}.`, + body: `Complete today's wellness check-in and ${status.incompleteTaskCount} remaining habit${status.incompleteTaskCount === 1 ? "" : "s"}.`, targetUrl, }; } @@ -129,7 +129,7 @@ export function buildDailyReminderNotification(status: DailyReminderStatus) { if (!status.quizCompleted) { return { title: "Time for your daily check-in", - body: "Complete today's wellness quiz to keep your bit in good spirits.", + body: "Complete today's wellness check-in to keep your bit in good spirits.", targetUrl, }; } diff --git a/lib/routes.ts b/lib/routes.ts index 0de94c2..0cb59fc 100644 --- a/lib/routes.ts +++ b/lib/routes.ts @@ -8,6 +8,7 @@ export const routes = { avatar: "/avatar", customize: "/customize", habits: "/habits", + dailyCheckIn: "/daily/check-in", dailyQuiz: "/daily/quiz", shop: "/shop", profile: "/profile",