Merge branch staging into mason
Resolve habit-tracker conflict by keeping the daily tasks countdown header and adopting check-in terminology from staging. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -61,25 +61,25 @@ function presetFromHex(id: string, name: string, hex: string): ColorPreset {
|
||||
|
||||
export const VARIANT_DISPLAY_NAMES: Record<string, string> = {
|
||||
"skin-1": "Default Form",
|
||||
"pants-1": "Slim Fit Jeans",
|
||||
"pants-2": "Rolled Cuff Shorts",
|
||||
"pants-3": "Pleated Skirt",
|
||||
"shoes-1": "Canvas Sneakers",
|
||||
"shoes-2": "Chunky High-Tops",
|
||||
"shoes-3": "Neon Glow Runners",
|
||||
"torso-1": "Crew Neck Tee",
|
||||
"torso-2": "Zip-Up Hoodie",
|
||||
"torso-3": "Button-Up Shirt",
|
||||
"torso-4": "Crimson Blouse",
|
||||
"eyes-1": "Round Gentle Eyes",
|
||||
"eyes-2": "Starry Spark Eyes",
|
||||
"head-1": "Spiked Mohawk",
|
||||
"head-2": "Cozy Knit Beanie",
|
||||
"head-3": "Retro Snapback Cap",
|
||||
"head-4": "Long Hair",
|
||||
"head-5": "Shoulder-Length Hair",
|
||||
"head-6": "Flowing Side Locks",
|
||||
"head-7": "Messy Shag Cut",
|
||||
"pants-1": "Shorts",
|
||||
"pants-2": "Pants",
|
||||
"pants-3": "Skirt",
|
||||
"shoes-1": "Boots",
|
||||
"shoes-2": "Shoes",
|
||||
"shoes-3": "Loafers",
|
||||
"torso-1": "T-Shirt",
|
||||
"torso-2": "Button-up",
|
||||
"torso-3": "Vest",
|
||||
"torso-4": "Robe",
|
||||
"eyes-1": "Eyes",
|
||||
"eyes-2": "Eyelashes",
|
||||
"head-1": "Short",
|
||||
"head-2": "Short-medium",
|
||||
"head-3": "Mohawk",
|
||||
"head-4": "Long",
|
||||
"head-5": "Pixie",
|
||||
"head-6": "Middle-part",
|
||||
"head-7": "Bun",
|
||||
};
|
||||
|
||||
export function getVariantDisplayName(variantId: string, fallbackLabel = variantId) {
|
||||
|
||||
@@ -44,7 +44,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();
|
||||
@@ -193,6 +193,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<DailyEntrySummary[]> {
|
||||
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<DailyQuizSubmission | null> {
|
||||
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<AvatarCondition | null> {
|
||||
const [entry, completedHabitLabels] = await Promise.all([
|
||||
getDailyEntryForToday(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,25 +19,25 @@ export const ROOM_SCENE_IMAGE_PATH = (id: RoomBackgroundId) =>
|
||||
export const ROOM_BACKGROUNDS: RoomBackground[] = [
|
||||
{
|
||||
id: "room-1",
|
||||
name: "Sunny Field",
|
||||
name: "Plains",
|
||||
free: true,
|
||||
sceneImage: ROOM_SCENE_IMAGE_PATH("room-1"),
|
||||
},
|
||||
{
|
||||
id: "room-2",
|
||||
name: "Golden Dusk",
|
||||
name: "Sunset",
|
||||
free: true,
|
||||
sceneImage: ROOM_SCENE_IMAGE_PATH("room-2"),
|
||||
},
|
||||
{
|
||||
id: "room-3",
|
||||
name: "Starlit Night",
|
||||
name: "Forest",
|
||||
free: false,
|
||||
sceneImage: ROOM_SCENE_IMAGE_PATH("room-3"),
|
||||
},
|
||||
{
|
||||
id: "room-4",
|
||||
name: "Mystic Forest",
|
||||
name: "Night",
|
||||
free: false,
|
||||
sceneImage: ROOM_SCENE_IMAGE_PATH("room-4"),
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ export const routes = {
|
||||
avatar: "/avatar",
|
||||
customize: "/customize",
|
||||
habits: "/habits",
|
||||
dailyCheckIn: "/daily/check-in",
|
||||
dailyQuiz: "/daily/quiz",
|
||||
shop: "/shop",
|
||||
profile: "/profile",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { isShopOnlyStyleVariant } from "@/lib/character/variant-access";
|
||||
import type { CharacterLayerId } from "@/lib/character/presets";
|
||||
import { CHARACTER_LAYERS, NONE_VARIANT_ID } from "@/lib/character/presets";
|
||||
import { getShopRoomBackgrounds } from "@/lib/room-backgrounds";
|
||||
import {
|
||||
CHARACTER_LAYERS,
|
||||
getVariantDisplayName,
|
||||
NONE_VARIANT_ID,
|
||||
} from "@/lib/character/presets";
|
||||
import { getRoomBackground, getShopRoomBackgrounds } from "@/lib/room-backgrounds";
|
||||
|
||||
export type ShopLayerId = Exclude<CharacterLayerId, "skin">;
|
||||
|
||||
@@ -95,6 +99,14 @@ export function getLayerLabel(layerId: ShopLayerId) {
|
||||
return CHARACTER_LAYERS.find((layer) => layer.id === layerId)?.label ?? layerId;
|
||||
}
|
||||
|
||||
export function getShopItemDisplayName(item: ShopItemRecord): string {
|
||||
if (item.type === "room") {
|
||||
return getRoomBackground(item.id).name;
|
||||
}
|
||||
|
||||
return getVariantDisplayName(item.id, item.name);
|
||||
}
|
||||
|
||||
export function findShopItemById(
|
||||
items: readonly ShopItemRecord[],
|
||||
itemId: string,
|
||||
|
||||
Reference in New Issue
Block a user