Task 25: Multiple dashboards — dashboards table + migration, /d/[slug] route, root redirect, dashboard tabs in nav with create/rename/delete/ set-default. Task 26: Editable dashboard — react-grid-layout drag/resize editor, widget picker modal with per-widget configurator auto-generated from default config, save/reset server actions with Zod validation. Completion visibility: server-side per-user setting (hours) replaces localStorage timer; queries filter completed items by updatedAt cutoff; Settings page dropdown persists preference via server action. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
import type { Metadata } from "next";
|
|
import "./globals.css";
|
|
import { Geist } from "next/font/google";
|
|
import { cn } from "@/lib/utils";
|
|
import { AppNav } from "@/components/app-nav";
|
|
import "@/modules"; // registers all module manifests
|
|
import { auth } from "@/lib/auth";
|
|
import { db } from "@/lib/db";
|
|
import { asc, eq } from "drizzle-orm";
|
|
import { dashboards, users } from "@/modules/_core/schema";
|
|
import type { DashboardMeta } from "@/app/d/actions";
|
|
import { getQuickAdds } from "@/modules/_core";
|
|
import { QuickAddProvider } from "@/components/quick-add-provider";
|
|
import { QuickAddSheet } from "@/components/quick-add-sheet";
|
|
import { CommandPalette } from "@/components/command-palette";
|
|
|
|
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" });
|
|
|
|
export const metadata: Metadata = {
|
|
title: "famapp",
|
|
description: "Family coordination app",
|
|
};
|
|
|
|
// Runs before paint — reads localStorage / prefers-color-scheme and applies
|
|
// data-theme + dark class to <html> so signed-out pages also get the right theme.
|
|
const prePaintScript = `(function(){
|
|
try {
|
|
var t = localStorage.getItem('theme') || 'default';
|
|
var m = localStorage.getItem('themeMode') || 'system';
|
|
var dark = m === 'dark' || (m === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
if (dark) document.documentElement.classList.add('dark');
|
|
else document.documentElement.classList.remove('dark');
|
|
} catch(e) {}
|
|
})();`;
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
let theme = "default";
|
|
let themeMode = "system";
|
|
let userDashboards: DashboardMeta[] = [];
|
|
|
|
const session = await auth();
|
|
if (session?.user?.id) {
|
|
const [row] = await db
|
|
.select({ theme: users.theme, themeMode: users.themeMode })
|
|
.from(users)
|
|
.where(eq(users.id, session.user.id))
|
|
.limit(1);
|
|
if (row) {
|
|
theme = row.theme;
|
|
themeMode = row.themeMode;
|
|
}
|
|
userDashboards = await db
|
|
.select({ id: dashboards.id, name: dashboards.name, slug: dashboards.slug, isDefault: dashboards.isDefault, position: dashboards.position })
|
|
.from(dashboards)
|
|
.where(eq(dashboards.userId, session.user.id))
|
|
.orderBy(asc(dashboards.position), asc(dashboards.createdAt));
|
|
}
|
|
|
|
// For system mode we can't know the preference on the server — the inline
|
|
// script will correct it before paint. We optimistically render light here.
|
|
const isDark = themeMode === "dark";
|
|
|
|
const quickAdds = getQuickAdds();
|
|
|
|
return (
|
|
<html
|
|
lang="en"
|
|
data-theme={theme}
|
|
className={cn("font-sans", geist.variable, isDark ? "dark" : "")}
|
|
>
|
|
<head>
|
|
<script dangerouslySetInnerHTML={{ __html: prePaintScript }} />
|
|
</head>
|
|
<body className="min-h-screen">
|
|
<QuickAddProvider actions={quickAdds}>
|
|
<AppNav dashboards={userDashboards} />
|
|
<main>{children}</main>
|
|
<QuickAddSheet />
|
|
<CommandPalette />
|
|
</QuickAddProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|