chore: server-and-serverless template
This commit is contained in:
118
apps/app/src/routes/__root.tsx
Normal file
118
apps/app/src/routes/__root.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
|
||||
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@pikku/mantine/core'
|
||||
import '@mantine/core/styles.css'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { PikkuProvider, createPikku } from '@pikku/react'
|
||||
import { PikkuFetch } from '@project/functions-sdk/pikku/pikku-fetch.gen'
|
||||
import { PikkuRPC } from '@project/functions-sdk/pikku/pikku-rpc.gen'
|
||||
import { activeTheme, buildTheme, themes, type Palette } from '@project/mantine-themes'
|
||||
import { type MantineThemeOverride } from '@pikku/mantine/core'
|
||||
import { defaultLocale, localeDir, supportedLocales, setActiveLocale } from '@/i18n/config'
|
||||
import { apiUrl } from '@/lib/env'
|
||||
import { PreferencesContext } from '@/contexts/preferences'
|
||||
|
||||
const LOCALE_KEY = 'app-locale'
|
||||
const THEME_KEY = 'app-theme'
|
||||
|
||||
// Root route owns the full HTML document for SSR (<html lang dir> so the tree
|
||||
// mirrors for RTL locales) and wraps the app in the Mantine + react-query +
|
||||
// Pikku providers. Mantine v8 SSR needs ColorSchemeScript in <head> and
|
||||
// mantineHtmlProps on <html>; @mantine/core/styles.css ships the static CSS.
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'Pikku App' },
|
||||
],
|
||||
}),
|
||||
component: RootDocument,
|
||||
})
|
||||
|
||||
function RootDocument() {
|
||||
// Server-safe defaults; useEffect syncs from localStorage after hydration.
|
||||
const [themeId, setThemeIdRaw] = useState('default')
|
||||
const [locale, setLocaleRaw] = useState(defaultLocale)
|
||||
// Transient override from fabric console live-preview (not persisted).
|
||||
const [previewTheme, setPreviewTheme] = useState<MantineThemeOverride | null>(null)
|
||||
|
||||
const effectiveTheme = previewTheme ?? themes[themeId] ?? activeTheme
|
||||
|
||||
// Sync preferences from localStorage after hydration.
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem(THEME_KEY)
|
||||
if (savedTheme && themes[savedTheme]) setThemeIdRaw(savedTheme)
|
||||
|
||||
const savedLocale = localStorage.getItem(LOCALE_KEY)
|
||||
if (savedLocale && supportedLocales.includes(savedLocale as (typeof supportedLocales)[number])) {
|
||||
setLocaleRaw(savedLocale)
|
||||
setActiveLocale(savedLocale as (typeof supportedLocales)[number])
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Keep <html lang dir> in sync with locale changes after hydration.
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale
|
||||
document.documentElement.dir = localeDir(locale)
|
||||
}, [locale])
|
||||
|
||||
// Fabric console live-preview: postMessage overrides the active theme
|
||||
// without persisting, so the user sees the palette instantly in an iframe.
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = event.data
|
||||
if (data?.source !== 'fabric-console' || data.type !== 'set-theme') return
|
||||
try {
|
||||
setPreviewTheme(data.palette ? buildTheme(data.palette as Palette) : null)
|
||||
} catch (err) {
|
||||
console.warn('[mantine-themes] ignoring bad theme payload', err)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => window.removeEventListener('message', onMessage)
|
||||
}, [])
|
||||
|
||||
const setLocale = (next: string) => {
|
||||
localStorage.setItem(LOCALE_KEY, next)
|
||||
setLocaleRaw(next)
|
||||
setActiveLocale(next as (typeof supportedLocales)[number])
|
||||
}
|
||||
|
||||
const setThemeId = (next: string) => {
|
||||
localStorage.setItem(THEME_KEY, next)
|
||||
setThemeIdRaw(next)
|
||||
// Clear any live-preview override so the persisted theme takes effect.
|
||||
setPreviewTheme(null)
|
||||
}
|
||||
|
||||
// Per-render instances keep server requests from sharing client/cache state.
|
||||
const [queryClient] = useState(() => new QueryClient())
|
||||
const [pikku] = useState(() =>
|
||||
createPikku(PikkuFetch, PikkuRPC, {
|
||||
serverUrl: apiUrl(),
|
||||
credentials: 'include',
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={localeDir(locale)} {...mantineHtmlProps}>
|
||||
<head>
|
||||
<HeadContent />
|
||||
<ColorSchemeScript defaultColorScheme="dark" />
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider theme={effectiveTheme} defaultColorScheme="dark">
|
||||
<PreferencesContext.Provider value={{ locale, themeId, setLocale, setThemeId }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PikkuProvider pikku={pikku}>
|
||||
<Outlet />
|
||||
</PikkuProvider>
|
||||
</QueryClientProvider>
|
||||
</PreferencesContext.Provider>
|
||||
</MantineProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
6
apps/app/src/routes/app.index.tsx
Normal file
6
apps/app/src/routes/app.index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { MessagePage } from '@/pages/MessagePage'
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: MessagePage,
|
||||
})
|
||||
12
apps/app/src/routes/app.tsx
Normal file
12
apps/app/src/routes/app.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { AppShell } from '@/components/AppShell'
|
||||
import { useRequireAuthentication } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
component: AppLayout,
|
||||
})
|
||||
|
||||
function AppLayout() {
|
||||
useRequireAuthentication()
|
||||
return <AppShell />
|
||||
}
|
||||
12
apps/app/src/routes/index.tsx
Normal file
12
apps/app/src/routes/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { LandingPage } from '@/pages/LandingPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: LandingRoute,
|
||||
})
|
||||
|
||||
function LandingRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <LandingPage />
|
||||
}
|
||||
12
apps/app/src/routes/login.tsx
Normal file
12
apps/app/src/routes/login.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { LoginPage } from '@/pages/LoginPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/login')({
|
||||
component: LoginRoute,
|
||||
})
|
||||
|
||||
function LoginRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <LoginPage />
|
||||
}
|
||||
12
apps/app/src/routes/signup.tsx
Normal file
12
apps/app/src/routes/signup.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { SignupPage } from '@/pages/SignupPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/signup')({
|
||||
component: SignupRoute,
|
||||
})
|
||||
|
||||
function SignupRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <SignupPage />
|
||||
}
|
||||
Reference in New Issue
Block a user