Fix hydration mismatches and theme bootstrap
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Fraunces, Manrope } from "next/font/google";
|
import { Fraunces, Manrope } from "next/font/google";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
import { SiteNav } from "@/components/site-nav";
|
import { SiteNav } from "@/components/site-nav";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
@@ -44,10 +45,10 @@ export default function RootLayout({
|
|||||||
className={`${headingFont.variable} ${bodyFont.variable} h-full antialiased`}
|
className={`${headingFont.variable} ${bodyFont.variable} h-full antialiased`}
|
||||||
suppressHydrationWarning
|
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">
|
<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">
|
<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">
|
<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>
|
<div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||||
|
|
||||||
import { getCategoryLabel, type CategoryValue } from "@/lib/categories";
|
import { getCategoryLabel, type CategoryValue } from "@/lib/categories";
|
||||||
import { getCurrentMonthKey } from "@/lib/date";
|
import { getCurrentMonthKey, getLocalToday } from "@/lib/date";
|
||||||
import { formatCurrencyFromCents } from "@/lib/money";
|
import { formatCurrencyFromCents } from "@/lib/money";
|
||||||
|
|
||||||
type SuggestionResponse = {
|
type SuggestionResponse = {
|
||||||
@@ -42,7 +42,7 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
|
|||||||
}>({
|
}>({
|
||||||
title: "",
|
title: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: "",
|
||||||
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
|
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
|
||||||
});
|
});
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
@@ -61,7 +61,13 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
|
|||||||
setExpenses(payload?.expenses ?? []);
|
setExpenses(payload?.expenses ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setFormState((current) => (current.date ? current : { ...current, date: getLocalToday() }));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
void loadExpenses();
|
void loadExpenses();
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const totalSpent = useMemo(
|
const totalSpent = useMemo(
|
||||||
@@ -121,7 +127,7 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
|
|||||||
setFormState({
|
setFormState({
|
||||||
title: "",
|
title: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: getLocalToday(),
|
||||||
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
|
category: (categoryOptions[0]?.value as CategoryValue | undefined) ?? "MISC",
|
||||||
});
|
});
|
||||||
setSuggestionMessage(null);
|
setSuggestionMessage(null);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type OllamaStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function HomeDashboard() {
|
export function HomeDashboard() {
|
||||||
const [selectedMonth, setSelectedMonth] = useState(getCurrentMonthKey());
|
const [selectedMonth, setSelectedMonth] = useState("");
|
||||||
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
|
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [insightBusy, setInsightBusy] = useState(false);
|
const [insightBusy, setInsightBusy] = useState(false);
|
||||||
@@ -68,6 +68,18 @@ export function HomeDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setSelectedMonth(getCurrentMonthKey());
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
void loadDashboard(selectedMonth);
|
void loadDashboard(selectedMonth);
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -155,7 +167,7 @@ export function HomeDashboard() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-stone-500 dark:text-stone-400">Month to date</p>
|
<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">
|
<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>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||||
|
|
||||||
import { getCurrentMonthKey } from "@/lib/date";
|
import { getCurrentMonthKey, getLocalToday } from "@/lib/date";
|
||||||
import { formatCurrencyFromCents } from "@/lib/money";
|
import { formatCurrencyFromCents } from "@/lib/money";
|
||||||
|
|
||||||
type PaycheckRecord = {
|
type PaycheckRecord = {
|
||||||
@@ -21,11 +21,11 @@ export function PaycheckWorkspace() {
|
|||||||
const [paychecks, setPaychecks] = useState<PaycheckRecord[]>([]);
|
const [paychecks, setPaychecks] = useState<PaycheckRecord[]>([]);
|
||||||
const [schedule, setSchedule] = useState<PaySchedule | null>(null);
|
const [schedule, setSchedule] = useState<PaySchedule | null>(null);
|
||||||
const [projectedDates, setProjectedDates] = useState<string[]>([]);
|
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 [showScheduleForm, setShowScheduleForm] = useState(false);
|
||||||
const [formState, setFormState] = useState({
|
const [formState, setFormState] = useState({
|
||||||
amount: "",
|
amount: "",
|
||||||
payDate: new Date().toISOString().slice(0, 10),
|
payDate: "",
|
||||||
});
|
});
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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();
|
void loadData();
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const totalIncome = useMemo(
|
const totalIncome = useMemo(
|
||||||
@@ -84,7 +92,7 @@ export function PaycheckWorkspace() {
|
|||||||
setSchedule(payload.schedule);
|
setSchedule(payload.schedule);
|
||||||
setProjectedDates(computeProjectedDates(payload.schedule.anchorDate, getCurrentMonthKey()));
|
setProjectedDates(computeProjectedDates(payload.schedule.anchorDate, getCurrentMonthKey()));
|
||||||
setShowScheduleForm(false);
|
setShowScheduleForm(false);
|
||||||
setScheduleForm({ amount: "", anchorDate: new Date().toISOString().slice(0, 10) });
|
setScheduleForm({ amount: "", anchorDate: getLocalToday() });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClearSchedule() {
|
async function handleClearSchedule() {
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ export function ThemeToggle() {
|
|||||||
const [isDark, setIsDark] = useState(false);
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDark(document.documentElement.classList.contains("dark"));
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setIsDark(document.documentElement.classList.contains("dark"));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
|||||||
Reference in New Issue
Block a user