Fix hydration mismatches and theme bootstrap

This commit is contained in:
2026-03-23 22:40:57 -04:00
parent e3ac732b1b
commit 8dcddbf278
5 changed files with 44 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Fraunces, Manrope } from "next/font/google";
import Script from "next/script";
import { SiteNav } from "@/components/site-nav";
import { ThemeToggle } from "@/components/theme-toggle";
@@ -44,10 +45,10 @@ export default function RootLayout({
className={`${headingFont.variable} ${bodyFont.variable} h-full antialiased`}
suppressHydrationWarning
>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="min-h-full bg-[linear-gradient(180deg,#f8f3ea_0%,#f5efe4_28%,#fbfaf7_100%)] text-stone-950 dark:bg-[linear-gradient(180deg,#1a1714_0%,#1c1a17_28%,#1e1c19_100%)] dark:text-stone-100">
<Script id="theme-script" strategy="beforeInteractive">
{themeScript}
</Script>
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8">
<header className="mb-10 flex flex-col gap-4 rounded-[2rem] border border-white/70 bg-white/80 px-6 py-5 shadow-[0_20px_50px_rgba(120,90,50,0.08)] backdrop-blur sm:flex-row sm:items-center sm:justify-between dark:border-stone-700/60 dark:bg-stone-900/80">
<div>

View File

@@ -3,7 +3,7 @@
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { getCategoryLabel, type CategoryValue } from "@/lib/categories";
import { getCurrentMonthKey } from "@/lib/date";
import { getCurrentMonthKey, getLocalToday } from "@/lib/date";
import { formatCurrencyFromCents } from "@/lib/money";
type SuggestionResponse = {
@@ -42,7 +42,7 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
}>({
title: "",
amount: "",
date: new Date().toISOString().slice(0, 10),
date: "",
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
});
const [editingId, setEditingId] = useState<string | null>(null);
@@ -61,7 +61,13 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
setExpenses(payload?.expenses ?? []);
}
const timeoutId = window.setTimeout(() => {
setFormState((current) => (current.date ? current : { ...current, date: getLocalToday() }));
}, 0);
void loadExpenses();
return () => window.clearTimeout(timeoutId);
}, []);
const totalSpent = useMemo(
@@ -121,7 +127,7 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
setFormState({
title: "",
amount: "",
date: new Date().toISOString().slice(0, 10),
date: getLocalToday(),
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
});
setSuggestionMessage(null);

View File

@@ -41,7 +41,7 @@ type OllamaStatus = {
};
export function HomeDashboard() {
const [selectedMonth, setSelectedMonth] = useState(getCurrentMonthKey());
const [selectedMonth, setSelectedMonth] = useState("");
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
const [insightBusy, setInsightBusy] = useState(false);
@@ -68,6 +68,18 @@ export function HomeDashboard() {
}
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setSelectedMonth(getCurrentMonthKey());
}, 0);
return () => window.clearTimeout(timeoutId);
}, []);
useEffect(() => {
if (!selectedMonth) {
return;
}
const timeoutId = window.setTimeout(() => {
void loadDashboard(selectedMonth);
}, 0);
@@ -155,7 +167,7 @@ export function HomeDashboard() {
<div>
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-stone-500 dark:text-stone-400">Month to date</p>
<h2 className="mt-2 text-3xl font-semibold text-stone-950 dark:text-white">
{snapshot ? getMonthLabel(snapshot.month) : getMonthLabel(selectedMonth)}
{snapshot ? getMonthLabel(snapshot.month) : selectedMonth ? getMonthLabel(selectedMonth) : "Loading current month..."}
</h2>
</div>
<input

View File

@@ -2,7 +2,7 @@
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { getCurrentMonthKey } from "@/lib/date";
import { getCurrentMonthKey, getLocalToday } from "@/lib/date";
import { formatCurrencyFromCents } from "@/lib/money";
type PaycheckRecord = {
@@ -21,11 +21,11 @@ export function PaycheckWorkspace() {
const [paychecks, setPaychecks] = useState<PaycheckRecord[]>([]);
const [schedule, setSchedule] = useState<PaySchedule | null>(null);
const [projectedDates, setProjectedDates] = useState<string[]>([]);
const [scheduleForm, setScheduleForm] = useState({ amount: "", anchorDate: new Date().toISOString().slice(0, 10) });
const [scheduleForm, setScheduleForm] = useState({ amount: "", anchorDate: "" });
const [showScheduleForm, setShowScheduleForm] = useState(false);
const [formState, setFormState] = useState({
amount: "",
payDate: new Date().toISOString().slice(0, 10),
payDate: "",
});
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -46,7 +46,15 @@ export function PaycheckWorkspace() {
}
}
const timeoutId = window.setTimeout(() => {
const today = getLocalToday();
setScheduleForm((current) => (current.anchorDate ? current : { ...current, anchorDate: today }));
setFormState((current) => (current.payDate ? current : { ...current, payDate: today }));
}, 0);
void loadData();
return () => window.clearTimeout(timeoutId);
}, []);
const totalIncome = useMemo(
@@ -84,7 +92,7 @@ export function PaycheckWorkspace() {
setSchedule(payload.schedule);
setProjectedDates(computeProjectedDates(payload.schedule.anchorDate, getCurrentMonthKey()));
setShowScheduleForm(false);
setScheduleForm({ amount: "", anchorDate: new Date().toISOString().slice(0, 10) });
setScheduleForm({ amount: "", anchorDate: getLocalToday() });
}
async function handleClearSchedule() {

View File

@@ -6,7 +6,11 @@ export function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setIsDark(document.documentElement.classList.contains("dark"));
}, 0);
return () => window.clearTimeout(timeoutId);
}, []);
function toggle() {