chore: server-and-serverless template

This commit is contained in:
e2e
2026-06-28 17:08:26 +02:00
commit 31e3be7352
202 changed files with 9063 additions and 0 deletions

View 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>
)
}

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { MessagePage } from '@/pages/MessagePage'
export const Route = createFileRoute('/app/')({
component: MessagePage,
})

View 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 />
}

View 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 />
}

View 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 />
}

View 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 />
}