Finish interrupted features: tab prefetch, pet viewport, and home logo.
Add shared quiz/habits tab cache with nav hover prefetch and AppDataPrefetcher, make the pet screen fill and center the viewport, and point the HaBit logo to /.
This commit is contained in:
@@ -8,6 +8,8 @@ export default function AvatarPage() {
|
||||
<AppShell
|
||||
title="Your Habit Pet"
|
||||
description="Watch your pet roam, breathe, and react to how you care for them."
|
||||
fillViewport
|
||||
centered
|
||||
>
|
||||
<Suspense fallback={<p className="text-sm text-muted-foreground">Loading...</p>}>
|
||||
<AvatarPageContent />
|
||||
|
||||
@@ -116,6 +116,21 @@
|
||||
max-width: calc(32rem - 2.5rem - 2rem);
|
||||
}
|
||||
|
||||
.pet-viewport-stage {
|
||||
@apply flex min-h-0 w-full flex-1 flex-col items-center justify-center;
|
||||
}
|
||||
|
||||
.tamagotchi-shell-fill {
|
||||
@apply flex min-h-0 w-full max-w-4xl flex-1 flex-col;
|
||||
}
|
||||
|
||||
.tamagotchi-lcd-pet-fill {
|
||||
@apply relative min-h-0 w-full flex-1;
|
||||
min-height: 16rem;
|
||||
max-width: none;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
.customize-body {
|
||||
min-height: 26rem;
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@ export function AvatarPageContent() {
|
||||
);
|
||||
}
|
||||
|
||||
return <PetHabitat customization={customization} />;
|
||||
return <PetHabitat customization={customization} fillViewport />;
|
||||
}
|
||||
|
||||
@@ -26,26 +26,45 @@ import {
|
||||
type DailyQuizSubmission,
|
||||
} from "@/lib/avatar-state";
|
||||
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||
import {
|
||||
getCachedQuizTabData,
|
||||
prefetchQuizTabData,
|
||||
} from "@/lib/app-tab-data-cache";
|
||||
import {
|
||||
getAvatarConditionForToday,
|
||||
getDailyEntryForToday,
|
||||
saveDailyEntry,
|
||||
} from "@/lib/daily-quiz-storage";
|
||||
import { JOURNAL_MAX_LENGTH } from "@/lib/journal-safety";
|
||||
|
||||
export function DailyQuizForm() {
|
||||
const cachedQuiz = getCachedQuizTabData();
|
||||
const [answers, setAnswers] = useState<DailyQuizAnswers>(
|
||||
defaultDailyQuizAnswers,
|
||||
cachedQuiz?.submission?.answers ?? defaultDailyQuizAnswers,
|
||||
);
|
||||
const [journal, setJournal] = useState("");
|
||||
const [journal, setJournal] = useState(cachedQuiz?.submission?.journal ?? "");
|
||||
const [submission, setSubmission] = useState<DailyQuizSubmission | null>(
|
||||
null,
|
||||
cachedQuiz?.submission ?? null,
|
||||
);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isReady, setIsReady] = useState(cachedQuiz !== null);
|
||||
const [liveCondition, setLiveCondition] = useState(
|
||||
submission?.condition ?? null,
|
||||
cachedQuiz?.liveCondition ?? cachedQuiz?.submission?.condition ?? null,
|
||||
);
|
||||
|
||||
const applyQuizData = useCallback(
|
||||
(data: NonNullable<ReturnType<typeof getCachedQuizTabData>>) => {
|
||||
if (data.submission) {
|
||||
setSubmission(data.submission);
|
||||
setAnswers(data.submission.answers);
|
||||
setJournal(data.submission.journal);
|
||||
setLiveCondition(data.submission.condition);
|
||||
} else {
|
||||
setSubmission(null);
|
||||
setLiveCondition(data.liveCondition);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const refreshLiveCondition = useCallback(async () => {
|
||||
@@ -53,31 +72,40 @@ export function DailyQuizForm() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadDailyCheckIn() {
|
||||
try {
|
||||
const existingEntry = await getDailyEntryForToday();
|
||||
const data = await prefetchQuizTabData();
|
||||
|
||||
if (existingEntry) {
|
||||
setSubmission(existingEntry);
|
||||
setAnswers(existingEntry.answers);
|
||||
setJournal(existingEntry.journal);
|
||||
setLiveCondition(existingEntry.condition);
|
||||
} else {
|
||||
await refreshLiveCondition();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyQuizData(data);
|
||||
} catch (loadError) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "Could not load today's check-in.",
|
||||
);
|
||||
} finally {
|
||||
setIsReady(true);
|
||||
if (!cancelled) {
|
||||
setIsReady(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadDailyCheckIn();
|
||||
}, [refreshLiveCondition]);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [applyQuizData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDataUpdated = () => {
|
||||
|
||||
@@ -17,12 +17,12 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||
import {
|
||||
getDailyTasks,
|
||||
type DailyTask,
|
||||
} from "@/lib/daily-tasks";
|
||||
getCachedHabitsTabData,
|
||||
prefetchHabitsTabData,
|
||||
} from "@/lib/app-tab-data-cache";
|
||||
import type { DailyTask } from "@/lib/daily-tasks";
|
||||
import {
|
||||
addHabit,
|
||||
getCustomHabits,
|
||||
getTodayDateKey,
|
||||
isHabitCompletedToday,
|
||||
removeHabit,
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
} from "@/lib/habits-storage";
|
||||
import { formatCoinsDelta } from "@/lib/coins";
|
||||
import { describeHabitWellnessBoost } from "@/lib/habit-wellness-effects";
|
||||
import { getProfilePreferences } from "@/lib/profile-preferences-storage";
|
||||
import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage";
|
||||
|
||||
type HabitTrackerProps = {
|
||||
mode?: "daily" | "manage" | "all";
|
||||
@@ -184,13 +182,22 @@ function CustomHabitList({
|
||||
}
|
||||
|
||||
export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
||||
const [dailyTasks, setDailyTasks] = useState<DailyTask[]>([]);
|
||||
const [customHabits, setCustomHabits] = useState<Habit[]>([]);
|
||||
const cachedHabits = getCachedHabitsTabData();
|
||||
const [dailyTasks, setDailyTasks] = useState<DailyTask[]>(
|
||||
cachedHabits?.dailyTasks ?? [],
|
||||
);
|
||||
const [customHabits, setCustomHabits] = useState<Habit[]>(
|
||||
cachedHabits?.customHabits ?? [],
|
||||
);
|
||||
const [newHabitLabel, setNewHabitLabel] = useState("");
|
||||
const [focusTopic, setFocusTopic] = useState<string | null>(null);
|
||||
const [quizCompletedToday, setQuizCompletedToday] = useState(false);
|
||||
const [focusTopic, setFocusTopic] = useState<string | null>(
|
||||
cachedHabits?.focusTopic ?? null,
|
||||
);
|
||||
const [quizCompletedToday, setQuizCompletedToday] = useState(
|
||||
cachedHabits?.quizCompletedToday ?? false,
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isReady, setIsReady] = useState(cachedHabits !== null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [optimisticCompletion, setOptimisticCompletion] = useState<
|
||||
Record<string, boolean>
|
||||
@@ -223,17 +230,21 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
||||
[toggleErrors],
|
||||
);
|
||||
|
||||
const applyHabitsData = useCallback(
|
||||
(data: NonNullable<ReturnType<typeof getCachedHabitsTabData>>) => {
|
||||
setDailyTasks(data.dailyTasks);
|
||||
setCustomHabits(data.customHabits);
|
||||
setFocusTopic(data.focusTopic);
|
||||
setQuizCompletedToday(data.quizCompletedToday);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const [tasks, customs] = await Promise.all([
|
||||
getDailyTasks(),
|
||||
getCustomHabits(),
|
||||
]);
|
||||
setDailyTasks(tasks);
|
||||
setCustomHabits(customs);
|
||||
setFocusTopic((await getProfilePreferences()).focusTopic);
|
||||
setQuizCompletedToday(await hasCompletedDailyQuizToday());
|
||||
const data = await prefetchHabitsTabData({ force: true });
|
||||
applyHabitsData(data);
|
||||
setIsReady(true);
|
||||
} catch (refreshError) {
|
||||
setError(
|
||||
@@ -243,7 +254,27 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
||||
);
|
||||
setIsReady(true);
|
||||
}
|
||||
}, []);
|
||||
}, [applyHabitsData]);
|
||||
|
||||
useEffect(() => {
|
||||
void prefetchHabitsTabData()
|
||||
.then(applyHabitsData)
|
||||
.catch((refreshError) => {
|
||||
setError(
|
||||
refreshError instanceof Error
|
||||
? refreshError.message
|
||||
: "Could not load habits.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsReady(true);
|
||||
});
|
||||
|
||||
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
||||
return () => {
|
||||
window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
||||
};
|
||||
}, [applyHabitsData, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coinMessage) {
|
||||
@@ -290,15 +321,6 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
||||
};
|
||||
}, [refresh, todayKey]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
|
||||
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
||||
return () => {
|
||||
window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh);
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const handleToggle = (habitId: string) => {
|
||||
const habit = dailyTasks.find((task) => task.id === habitId);
|
||||
|
||||
|
||||
36
components/layout/app-data-prefetcher.tsx
Normal file
36
components/layout/app-data-prefetcher.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||
import {
|
||||
invalidateAppTabDataCache,
|
||||
prefetchAppTabData,
|
||||
} from "@/lib/app-tab-data-cache";
|
||||
|
||||
type AppDataPrefetcherProps = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export function AppDataPrefetcher({ enabled }: AppDataPrefetcherProps) {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
void prefetchAppTabData();
|
||||
|
||||
const handleDataUpdated = () => {
|
||||
invalidateAppTabDataCache();
|
||||
void prefetchAppTabData({ force: true });
|
||||
};
|
||||
|
||||
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appNavItems, routes, type NavItem } from "@/lib/routes";
|
||||
import {
|
||||
prefetchHabitsTabData,
|
||||
prefetchQuizTabData,
|
||||
} from "@/lib/app-tab-data-cache";
|
||||
|
||||
const gridColsClass = {
|
||||
1: "grid-cols-1",
|
||||
@@ -31,6 +35,15 @@ const navIcons = {
|
||||
[routes.profile]: UserRound,
|
||||
} as const;
|
||||
|
||||
const prefetchByRoute: Partial<Record<string, () => void>> = {
|
||||
[routes.habits]: () => {
|
||||
void prefetchHabitsTabData();
|
||||
},
|
||||
[routes.dailyQuiz]: () => {
|
||||
void prefetchQuizTabData();
|
||||
},
|
||||
};
|
||||
|
||||
type AppNavProps = {
|
||||
items?: readonly NavItem[];
|
||||
};
|
||||
@@ -76,6 +89,9 @@ export const AppNav = forwardRef<HTMLElement, AppNavProps>(function AppNav(
|
||||
href={item.href}
|
||||
aria-label={item.label}
|
||||
title={item.label}
|
||||
prefetch
|
||||
onPointerEnter={() => prefetchByRoute[item.href]?.()}
|
||||
onFocus={() => prefetchByRoute[item.href]?.()}
|
||||
className={cn(
|
||||
"bottom-nav-item flex flex-col items-center justify-center gap-1.5 overflow-hidden border-2 px-1 py-2 text-[9px] leading-none transition-colors duration-75",
|
||||
isActive
|
||||
|
||||
@@ -7,6 +7,10 @@ type AppShellProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
wide?: boolean;
|
||||
/** Use remaining viewport height between top bar and bottom nav. */
|
||||
fillViewport?: boolean;
|
||||
/** Center page content horizontally (pairs well with fillViewport). */
|
||||
centered?: boolean;
|
||||
};
|
||||
|
||||
const contentWidthClass = {
|
||||
@@ -19,20 +23,33 @@ export function AppShell({
|
||||
title,
|
||||
description,
|
||||
wide = false,
|
||||
fillViewport = false,
|
||||
centered = false,
|
||||
}: AppShellProps) {
|
||||
return (
|
||||
<div className="relative min-h-dvh bg-background bg-[radial-gradient(circle_at_20%_0%,hsl(var(--primary)/0.12)_0%,transparent_45%),radial-gradient(circle_at_80%_100%,hsl(var(--secondary)/0.1)_0%,transparent_40%)] pt-topbar">
|
||||
<div className="relative flex min-h-dvh flex-col bg-background bg-[radial-gradient(circle_at_20%_0%,hsl(var(--primary)/0.12)_0%,transparent_45%),radial-gradient(circle_at_80%_100%,hsl(var(--secondary)/0.1)_0%,transparent_40%)] pt-topbar">
|
||||
<AppTopBar />
|
||||
<main
|
||||
className={cn(
|
||||
"mx-auto px-4 py-6 lg:px-5",
|
||||
"mx-auto flex w-full flex-1 flex-col px-4 py-6 lg:px-5",
|
||||
fillViewport
|
||||
? "min-h-[calc(100dvh-var(--app-topbar-height)-var(--app-nav-offset)-env(safe-area-inset-top,0px))]"
|
||||
: null,
|
||||
centered ? "items-center" : null,
|
||||
wide ? contentWidthClass.wide : contentWidthClass.default,
|
||||
fillViewport && "max-w-none lg:max-w-none",
|
||||
)}
|
||||
>
|
||||
{title ? (
|
||||
<AppPageHeader title={title} description={description} />
|
||||
) : null}
|
||||
{children}
|
||||
<div
|
||||
className={cn(
|
||||
fillViewport ? "pet-viewport-stage flex w-full flex-1 flex-col" : "contents",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,8 @@ type AppTopBarProps = {
|
||||
|
||||
export function AppTopBar({ className }: AppTopBarProps) {
|
||||
const pathname = usePathname();
|
||||
const isHome = pathname === routes.avatar;
|
||||
const isHome =
|
||||
pathname === routes.home || pathname === routes.avatar;
|
||||
|
||||
return (
|
||||
<header
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { AppNav } from "@/components/layout/app-nav";
|
||||
import { AppDataPrefetcher } from "@/components/layout/app-data-prefetcher";
|
||||
import {
|
||||
NavOffsetProvider,
|
||||
useNavOffsetRef,
|
||||
@@ -55,6 +56,7 @@ export function GlobalBottomNav() {
|
||||
|
||||
return (
|
||||
<NavOffsetProvider enabled={showNav}>
|
||||
<AppDataPrefetcher enabled={showNav} />
|
||||
{showNav ? <MeasuredAppNav /> : null}
|
||||
</NavOffsetProvider>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ type HabitBrandProps = {
|
||||
|
||||
export function HabitBrand({
|
||||
className,
|
||||
href = routes.avatar,
|
||||
href = routes.home,
|
||||
"aria-current": ariaCurrent,
|
||||
}: HabitBrandProps) {
|
||||
const [useText, setUseText] = useState(false);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { cn } from "@/lib/utils";
|
||||
type PetHabitatProps = {
|
||||
customization: AvatarCustomization;
|
||||
className?: string;
|
||||
fillViewport?: boolean;
|
||||
};
|
||||
|
||||
const MOOD_LABELS = {
|
||||
@@ -29,7 +30,11 @@ const MOOD_LABELS = {
|
||||
tired: "SLEEPY",
|
||||
} as const;
|
||||
|
||||
export function PetHabitat({ customization, className }: PetHabitatProps) {
|
||||
export function PetHabitat({
|
||||
customization,
|
||||
className,
|
||||
fillViewport = false,
|
||||
}: PetHabitatProps) {
|
||||
const [condition, setCondition] = useState<AvatarCondition>(
|
||||
defaultAvatarCondition,
|
||||
);
|
||||
@@ -45,9 +50,20 @@ export function PetHabitat({ customization, className }: PetHabitatProps) {
|
||||
void loadStatus();
|
||||
}, []);
|
||||
|
||||
const shellClassName = cn(
|
||||
"tamagotchi-shell overflow-hidden p-3 sm:p-4",
|
||||
fillViewport && "tamagotchi-shell-fill",
|
||||
className,
|
||||
);
|
||||
|
||||
const lcdClassName = cn(
|
||||
"tamagotchi-lcd relative",
|
||||
fillViewport ? "tamagotchi-lcd-pet-fill" : "tamagotchi-lcd-pet-match",
|
||||
);
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className={cn("tamagotchi-shell p-4", className)}>
|
||||
<div className={shellClassName}>
|
||||
<p className="text-center text-[10px] text-muted-foreground">
|
||||
Waking up pet...
|
||||
</p>
|
||||
@@ -56,15 +72,15 @@ export function PetHabitat({ customization, className }: PetHabitatProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("tamagotchi-shell overflow-hidden p-3 sm:p-4", className)}>
|
||||
<div className="mb-3 border-b-2 border-border/60 pb-3">
|
||||
<div className={shellClassName}>
|
||||
<div className="mb-3 shrink-0 border-b-2 border-border/60 pb-3">
|
||||
<p className="text-sm text-foreground">{customization.name}</p>
|
||||
<p className="mt-1 text-[9px] uppercase tracking-[0.25em] text-secondary">
|
||||
{MOOD_LABELS[condition.mood]}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="tamagotchi-lcd tamagotchi-lcd-pet-match relative">
|
||||
<div className={lcdClassName}>
|
||||
<ParallaxRoomBackground roomId={roomId as RoomBackgroundId} />
|
||||
<AnimatedPetSprite customization={customization} />
|
||||
<div className="pointer-events-none absolute left-2 top-2 z-20 grid gap-1.5">
|
||||
|
||||
122
lib/app-tab-data-cache.ts
Normal file
122
lib/app-tab-data-cache.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { AvatarCondition, DailyQuizSubmission } from "@/lib/avatar-state";
|
||||
import type { DailyTask } from "@/lib/daily-tasks";
|
||||
import { getDailyTasks } from "@/lib/daily-tasks";
|
||||
import {
|
||||
getAvatarConditionForToday,
|
||||
getDailyEntryForToday,
|
||||
hasCompletedDailyQuizToday,
|
||||
} from "@/lib/daily-quiz-storage";
|
||||
import { getCustomHabits, type Habit } from "@/lib/habits-storage";
|
||||
import { getProfilePreferences } from "@/lib/profile-preferences-storage";
|
||||
|
||||
export type QuizTabData = {
|
||||
submission: DailyQuizSubmission | null;
|
||||
liveCondition: AvatarCondition | null;
|
||||
};
|
||||
|
||||
export type HabitsTabData = {
|
||||
dailyTasks: DailyTask[];
|
||||
customHabits: Habit[];
|
||||
focusTopic: string | null;
|
||||
quizCompletedToday: boolean;
|
||||
};
|
||||
|
||||
let quizCache: QuizTabData | null = null;
|
||||
let habitsCache: HabitsTabData | null = null;
|
||||
let quizInflight: Promise<QuizTabData> | null = null;
|
||||
let habitsInflight: Promise<HabitsTabData> | null = null;
|
||||
|
||||
export function getCachedQuizTabData() {
|
||||
return quizCache;
|
||||
}
|
||||
|
||||
export function getCachedHabitsTabData() {
|
||||
return habitsCache;
|
||||
}
|
||||
|
||||
export function invalidateAppTabDataCache() {
|
||||
quizCache = null;
|
||||
habitsCache = null;
|
||||
}
|
||||
|
||||
async function loadQuizTabData(): Promise<QuizTabData> {
|
||||
const submission = await getDailyEntryForToday();
|
||||
|
||||
if (submission) {
|
||||
return {
|
||||
submission,
|
||||
liveCondition: submission.condition,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
submission: null,
|
||||
liveCondition: await getAvatarConditionForToday(),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadHabitsTabData(): Promise<HabitsTabData> {
|
||||
const [dailyTasks, customHabits, preferences, quizCompletedToday] =
|
||||
await Promise.all([
|
||||
getDailyTasks(),
|
||||
getCustomHabits(),
|
||||
getProfilePreferences(),
|
||||
hasCompletedDailyQuizToday(),
|
||||
]);
|
||||
|
||||
return {
|
||||
dailyTasks,
|
||||
customHabits,
|
||||
focusTopic: preferences.focusTopic,
|
||||
quizCompletedToday,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prefetchQuizTabData(options?: { force?: boolean }) {
|
||||
if (!options?.force && quizCache) {
|
||||
return quizCache;
|
||||
}
|
||||
|
||||
if (quizInflight) {
|
||||
return quizInflight;
|
||||
}
|
||||
|
||||
quizInflight = loadQuizTabData()
|
||||
.then((data) => {
|
||||
quizCache = data;
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
quizInflight = null;
|
||||
});
|
||||
|
||||
return quizInflight;
|
||||
}
|
||||
|
||||
export async function prefetchHabitsTabData(options?: { force?: boolean }) {
|
||||
if (!options?.force && habitsCache) {
|
||||
return habitsCache;
|
||||
}
|
||||
|
||||
if (habitsInflight) {
|
||||
return habitsInflight;
|
||||
}
|
||||
|
||||
habitsInflight = loadHabitsTabData()
|
||||
.then((data) => {
|
||||
habitsCache = data;
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
habitsInflight = null;
|
||||
});
|
||||
|
||||
return habitsInflight;
|
||||
}
|
||||
|
||||
export async function prefetchAppTabData(options?: { force?: boolean }) {
|
||||
await Promise.all([
|
||||
prefetchQuizTabData(options),
|
||||
prefetchHabitsTabData(options),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user