"use client"; import dynamic from "next/dynamic"; import { useEffect, type ComponentType } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { SettingsCard } from "@/components/settings/settings-card"; import type { SettingsSectionSlug } from "@/components/settings/section-slugs"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; /** * One settings section, loaded on its own. * * The section route renders exactly one of twenty-one sections, but importing * them statically put all twenty-one (and everything under them: the import * panel, the integration cards, the MCP and API token tables, the score * editor) into the route's entry bundle, so opening "About" downloaded the * whole settings tree. Each section is now its own chunk. A first load still * renders the section on the server (`ssr` stays on), so nothing jumps; the * placeholder below only shows while a client-side switch between sections * fetches the next chunk. */ /** Props a section takes. Only Integrations takes any. */ interface SectionProps { callbackUrls?: IntegrationCallbackUrls; } function SectionLoading() { // The settings card shell itself, so the section does not move when the // real cards arrive (UI standards ยง13: a loading state paints the card). return (
); } /** * One import per section, shared by the lazy component below and by the * preload. One call site per file keeps the bundler to one chunk per section; * a second `import()` of the same file elsewhere emitted a second copy. */ const LOADERS: Record< SettingsSectionSlug, () => Promise> > = { account: () => import("@/components/settings/account-section").then( (m) => m.AccountSection, ), access: () => import("@/components/settings/access-section").then((m) => m.AccessSection), security: () => import("@/components/settings/security-section").then( (m) => m.SecuritySection, ), modules: () => import("@/components/settings/modules-section").then( (m) => m.ModulesSection, ), about: () => import("@/components/settings/about-section").then((m) => m.AboutSection), ai: () => import("@/components/settings/ai-section").then((m) => m.AiSection), coach: () => import("@/components/settings/coach-section").then((m) => m.CoachSection), integrations: () => import("@/components/settings/integrations-section").then( (m) => function IntegrationsWithUrls({ callbackUrls }: SectionProps) { // The route always passes the server-resolved URLs; the guard // keeps the section's own prop contract required. if (!callbackUrls) return null; return ; }, ), sources: () => import("@/components/settings/sources-section").then( (m) => m.SourcesSection, ), notifications: () => import("@/components/settings/notifications-section").then( (m) => m.NotificationsSection, ), layout: () => import("@/components/settings/layout-section").then((m) => m.LayoutSection), environment: () => import("@/components/settings/environment-section").then( (m) => m.EnvironmentSection, ), anamnesis: () => import("@/components/settings/anamnesis-section").then( (m) => m.AnamnesisSection, ), score: () => import("@/components/settings/score-section").then((m) => m.ScoreSection), thresholds: () => import("@/components/settings/thresholds-section").then( (m) => m.ThresholdsSection, ), api: () => import("@/components/settings/api-section").then((m) => m.ApiSection), mcp: () => import("@/components/settings/mcp-section").then((m) => m.McpSection), gesundheitsakte: () => import("@/components/settings/gesundheitsakte-section").then( (m) => m.GesundheitsakteSection, ), export: () => import("@/components/settings/export-section").then((m) => m.ExportSection), advanced: () => import("@/components/settings/advanced-section").then( (m) => m.AdvancedSection, ), privacy: () => import("@/components/settings/privacy-section").then( (m) => m.PrivacySection, ), }; const SECTIONS: Record> = { account: dynamic(LOADERS.account, { loading: () => }), access: dynamic(LOADERS.access, { loading: () => }), security: dynamic(LOADERS.security, { loading: () => }), modules: dynamic(LOADERS.modules, { loading: () => }), about: dynamic(LOADERS.about, { loading: () => }), ai: dynamic(LOADERS.ai, { loading: () => }), coach: dynamic(LOADERS.coach, { loading: () => }), integrations: dynamic(LOADERS.integrations, { loading: () => , }), sources: dynamic(LOADERS.sources, { loading: () => }), notifications: dynamic(LOADERS.notifications, { loading: () => , }), layout: dynamic(LOADERS.layout, { loading: () => }), environment: dynamic(LOADERS.environment, { loading: () => , }), anamnesis: dynamic(LOADERS.anamnesis, { loading: () => }), score: dynamic(LOADERS.score, { loading: () => }), thresholds: dynamic(LOADERS.thresholds, { loading: () => , }), api: dynamic(LOADERS.api, { loading: () => }), mcp: dynamic(LOADERS.mcp, { loading: () => }), gesundheitsakte: dynamic(LOADERS.gesundheitsakte, { loading: () => , }), export: dynamic(LOADERS.export, { loading: () => }), advanced: dynamic(LOADERS.advanced, { loading: () => }), privacy: dynamic(LOADERS.privacy, { loading: () => }), }; /** * The section mounts only once the record gate has the account * (`RecordSettingsSectionGate`), so without a head start its chunk would be * fetched after that and the reader would see two loading states in a row. * This starts the fetch at hydration, beside the gate's own read; the module * cache makes the later lazy render of the same file free. */ export function SettingsSectionPreload({ section, }: { section: SettingsSectionSlug; }) { useEffect(() => { void LOADERS[section]().catch(() => { // The section's own lazy import retries and reports; a failed head // start changes nothing. }); }, [section]); return null; } export function SettingsSectionLoader({ section, callbackUrls, }: { section: SettingsSectionSlug; callbackUrls?: IntegrationCallbackUrls; }) { const Section = SECTIONS[section]; return
; }