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,103 @@
import {
AppShell as MantineAppShell,
Box,
Button,
Group,
NavLink,
Stack,
Text,
Title,
} from '@pikku/mantine/core'
import { Link, Outlet } from '@tanstack/react-router'
import { asI18n } from '@pikku/react'
import { useState } from 'react'
import { m } from '@/i18n/messages'
import { useLocale } from '@/i18n/config'
import { usePikkuQuery } from '@project/functions-sdk/pikku/api.gen'
import { signOut } from '@/lib/auth'
import { LanguageSelector } from './LanguageSelector'
import { ThemeSelector } from './ThemeSelector'
export function AppShell() {
useLocale()
const session = usePikkuQuery('getSession', {})
const [isSigningOut, setIsSigningOut] = useState(false)
return (
<MantineAppShell
header={{ height: 88 }}
navbar={{ width: 250, breakpoint: 'sm' }}
padding="lg"
styles={{
main: {
background:
'radial-gradient(circle at top left, rgba(96, 165, 250, 0.12), transparent 26%), linear-gradient(180deg, #10203a 0%, #07111e 56%, #040913 100%)',
},
}}
>
<MantineAppShell.Header px="lg" py="md">
<Group justify="space-between" align="center" h="100%">
<div>
<Text tt="uppercase" fw={700} fz="xs" c="blue.3" style={{ letterSpacing: '0.18em' }}>
{m.app_shell__eyebrow()}
</Text>
<Title order={2}>{m.app_shell__title()}</Title>
</div>
<Group gap="md">
<LanguageSelector />
<ThemeSelector />
<Box
px="md"
py="xs"
style={{
border: '1px solid var(--mantine-color-dark-4)',
borderRadius: '1rem',
background: 'rgba(255,255,255,0.03)',
}}
>
<Text size="xs" c="dimmed">
{session.data?.name ? asI18n(session.data.name) : m.app_shell__signed_in()}
</Text>
<Text fw={600}>
{session.data?.email ? asI18n(session.data.email) : m.common__loading()}
</Text>
</Box>
<Button
variant="light"
radius="md"
loading={isSigningOut}
onClick={async () => {
setIsSigningOut(true)
try {
await signOut()
window.location.href = '/login'
} finally {
setIsSigningOut(false)
}
}}
>
{m.app_shell__sign_out()}
</Button>
</Group>
</Group>
</MantineAppShell.Header>
<MantineAppShell.Navbar p="md">
<Stack gap="xs">
<NavLink
component={Link}
to="/app"
label={m.app_shell__nav__message()}
description={m.app_shell__nav__message_description()}
variant="filled"
activeOptions={{ exact: true }}
/>
</Stack>
</MantineAppShell.Navbar>
<MantineAppShell.Main>
<Outlet />
</MantineAppShell.Main>
</MantineAppShell>
)
}

View File

@@ -0,0 +1,105 @@
import type { FormEvent } from 'react'
import type { I18nNode, I18nString } from '@pikku/react'
import {
Box,
Button,
Card,
Container,
PasswordInput,
Stack,
Text,
TextInput,
Title,
} from '@pikku/mantine/core'
import { m } from '@/i18n/messages'
import { useLocale } from '@/i18n/config'
type AuthCardProps = {
title: I18nString
description: I18nString
cta: I18nString
email: string
password: string
// 'current-password' on login (let the browser fill a saved password),
// 'new-password' on signup (let it suggest a strong one).
passwordAutoComplete: 'current-password' | 'new-password'
busy: boolean
message: I18nString | null
error: I18nString | null
footer: I18nNode
onEmailChange: (value: string) => void
onPasswordChange: (value: string) => void
onSubmit: (event: FormEvent<HTMLFormElement>) => void
}
export function AuthCard(props: AuthCardProps) {
useLocale()
return (
<Box
mih="100vh"
style={{
background:
'radial-gradient(circle at top left, rgba(96, 165, 250, 0.16), transparent 30%), linear-gradient(180deg, #10203a 0%, #07111e 56%, #040913 100%)',
}}
py="xl"
>
<Container size={560}>
<Card radius="xl" padding="xl" withBorder shadow="xl" bg="rgba(10, 20, 36, 0.86)">
<Stack gap="lg">
<div>
<Text
tt="uppercase"
fw={700}
fz="xs"
c="blue.3"
mb="sm"
style={{ letterSpacing: '0.18em' }}
>
{m.auth__eyebrow()}
</Text>
<Title order={1}>{props.title}</Title>
<Text c="dimmed" mt="sm" maw={440}>
{props.description}
</Text>
</div>
<form onSubmit={props.onSubmit}>
<Stack gap="md">
<TextInput
label={m.common__email()}
placeholder={m.common__email_placeholder()}
value={props.email}
onChange={(event) => props.onEmailChange(event.currentTarget.value)}
required
radius="md"
type="email"
autoComplete="email"
/>
<PasswordInput
label={m.common__password()}
placeholder={m.common__password_placeholder()}
value={props.password}
onChange={(event) => props.onPasswordChange(event.currentTarget.value)}
required
radius="md"
autoComplete={props.passwordAutoComplete}
/>
<Button type="submit" loading={props.busy} radius="md" size="md">
{props.cta}
</Button>
</Stack>
</form>
{props.message ? <Text c="teal.3">{props.message}</Text> : null}
{props.error ? <Text c="red.3">{props.error}</Text> : null}
<Text c="dimmed" size="sm">
{props.footer}
</Text>
</Stack>
</Card>
</Container>
</Box>
)
}

View File

@@ -0,0 +1,32 @@
import { Select } from '@pikku/mantine/core'
import { m } from '@/i18n/messages'
import { useLocale } from '@/i18n/config'
import { supportedLocales } from '@/i18n/config'
import { usePreferences } from '@/contexts/preferences'
const LOCALE_LABELS: Record<string, string> = {
en: 'English',
fr: 'Français',
}
export function LanguageSelector() {
useLocale()
const { locale, setLocale } = usePreferences()
const data = supportedLocales.map((code) => ({
value: code,
label: LOCALE_LABELS[code] ?? code.toUpperCase(),
}))
return (
<Select
aria-label={m.preferences__language()}
data={data}
value={locale}
onChange={(v) => v && setLocale(v)}
size="xs"
w={110}
allowDeselect={false}
/>
)
}

View File

@@ -0,0 +1,51 @@
import { Card, Stack, Text, Title } from '@pikku/mantine/core'
import { asI18n } from '@pikku/react'
import { m } from '@/i18n/messages'
import { useLocale } from '@/i18n/config'
type MessageCardProps = {
message: string
updatedAt: string
updatedBy: {
email: string
name: string | null
} | null
}
export function MessageCard(props: MessageCardProps) {
useLocale()
const updatedBy = props.updatedBy?.name || props.updatedBy?.email || m.message__read__nobody()
return (
<Card radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
<Stack gap="lg">
<div>
<Text
tt="uppercase"
fw={700}
fz="xs"
c="blue.3"
mb="sm"
style={{ letterSpacing: '0.18em' }}
>
{m.message__read__eyebrow()}
</Text>
<Title order={2}>{m.message__read__title()}</Title>
<Text c="dimmed" mt="sm">
{m.message__read__description()}
</Text>
</div>
<Card radius="lg" padding="xl" bg="rgba(255,255,255,0.03)" withBorder>
<Stack gap="xs">
<Title order={1} fz="2.35rem">
{asI18n(props.message)}
</Title>
<Text c="dimmed">{m.message__read__last_updated_by({ name: updatedBy })}</Text>
<Text c="dimmed">{asI18n(new Date(props.updatedAt).toLocaleString())}</Text>
</Stack>
</Card>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,27 @@
import { Select } from '@pikku/mantine/core'
import { m } from '@/i18n/messages'
import { useLocale } from '@/i18n/config'
import { palettes } from '@project/mantine-themes'
import { usePreferences } from '@/contexts/preferences'
export function ThemeSelector() {
useLocale()
const { themeId, setThemeId } = usePreferences()
const data = Object.entries(palettes as Record<string, { name: string }>).map(([id, p]) => ({
value: id,
label: p.name,
}))
return (
<Select
aria-label={m.preferences__theme()}
data={data}
value={themeId}
onChange={(v) => v && setThemeId(v)}
size="xs"
w={110}
allowDeselect={false}
/>
)
}