chore: heygermany customer project
Some checks failed
Main / Setup and Test (push) Has been cancelled
Main / Build Website (push) Has been cancelled

This commit is contained in:
e2e
2026-07-11 11:30:11 +02:00
commit f0093328d8
370 changed files with 35601 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams } from '@tanstack/react-router'
import {
Button,
Container,
Group,
Paper,
Select,
Stack,
Text,
Textarea,
Title,
} from '@pikku/mantine/core'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { asI18n } from '@pikku/react'
import type {
BackendTranslationBundle,
BackendTranslationNamespace,
} from '@heygermany/sdk'
import { invokeRPC } from '@/pikku/rpc'
function toPrettyJson(value: Record<string, unknown>) {
return JSON.stringify(value, null, 2)
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
export default function BackOfficeTranslationsPage() {
const params = useParams({ strict: false })
const lang = params.lang as string
const queryClient = useQueryClient()
const [selectedLocale, setSelectedLocale] = useState<string | null>(null)
const [selectedNamespace, setSelectedNamespace] =
useState<BackendTranslationNamespace | null>(null)
const [editorValue, setEditorValue] = useState('')
const [editorError, setEditorError] = useState<string | null>(null)
const query = useQuery({
queryKey: ['backoffice', 'translations'],
queryFn: async () =>
await invokeRPC<BackendTranslationBundle[]>('getBackOfficeTranslations', {}),
})
const rows = query.data ?? []
const localeOptions = useMemo(
() => Array.from(new Set(rows.map((row) => row.locale))).map((locale) => ({
value: locale,
label: locale,
})),
[rows]
)
const namespaceOptions = useMemo(
() => Array.from(new Set(rows.map((row) => row.namespace))).map((namespace) => ({
value: namespace,
label: namespace,
})),
[rows]
)
useEffect(() => {
if (rows.length === 0) return
if (!selectedLocale) {
setSelectedLocale(rows.find((row) => Object.keys(row.content).length > 0)?.locale ?? rows[0].locale)
}
if (!selectedNamespace) {
setSelectedNamespace(
rows.find((row) => Object.keys(row.content).length > 0)?.namespace ?? rows[0].namespace
)
}
}, [rows, selectedLocale, selectedNamespace])
const selectedRow = useMemo(
() =>
rows.find(
(row) =>
row.locale === selectedLocale && row.namespace === selectedNamespace
) ?? null,
[rows, selectedLocale, selectedNamespace]
)
useEffect(() => {
if (!selectedRow) return
setEditorValue(toPrettyJson(selectedRow.content))
setEditorError(null)
}, [selectedRow])
const saveMutation = useMutation({
mutationFn: async () => {
if (!selectedLocale || !selectedNamespace) {
throw new Error('Select a locale and namespace first')
}
const parsed = JSON.parse(editorValue)
if (!isRecord(parsed)) {
throw new Error('Translation bundle must be a JSON object')
}
return await invokeRPC<BackendTranslationBundle>('updateBackOfficeTranslation', {
locale: selectedLocale,
namespace: selectedNamespace,
content: parsed,
})
},
onSuccess: async () => {
setEditorError(null)
await queryClient.invalidateQueries({
queryKey: ['backoffice', 'translations'],
})
},
onError: (error) => {
setEditorError(error instanceof Error ? error.message : String(error))
},
})
return (
<Container size="xl">
<Stack gap="lg">
<Group justify="space-between" align="flex-start">
<Stack gap={4}>
<Title order={1}>{asI18n('Backend translations')}</Title>
<Text c="dimmed">
{asI18n('Edit DB-backed candidate result copy. Changes apply live to backend result rendering.')}
</Text>
</Stack>
<Button component={Link} to={`/${lang}/backoffice/candidates`} variant="light">
{asI18n('Back to candidates')}
</Button>
</Group>
<Paper withBorder radius="md" p="lg">
<Stack gap="md">
<Group grow align="flex-start">
<Select
label={asI18n('Locale')}
data={localeOptions}
value={selectedLocale}
onChange={(value) => setSelectedLocale(value)}
searchable
/>
<Select
label={asI18n('Namespace')}
data={namespaceOptions}
value={selectedNamespace}
onChange={(value) =>
setSelectedNamespace(value as BackendTranslationNamespace | null)
}
/>
</Group>
<Textarea
label={asI18n('Translation JSON')}
autosize
minRows={24}
maxRows={40}
value={editorValue}
onChange={(event) => setEditorValue(event.currentTarget.value)}
placeholder={asI18n('{ }')}
/>
{selectedRow ? (
<Text size="sm" c="dimmed">
{asI18n(`Last updated: ${new Date(selectedRow.updatedAt).toLocaleString()}`)}
</Text>
) : null}
{editorError ? (
<Text size="sm" c="red">
{asI18n(editorError)}
</Text>
) : null}
<Group justify="space-between">
<Text size="sm" c="dimmed">
{query.isLoading
? asI18n('Loading translations...')
: asI18n(`${rows.length} translation bundles available`)}
</Text>
<Button
loading={saveMutation.isPending}
disabled={!selectedRow}
onClick={() => saveMutation.mutate()}
>
{asI18n('Save translation bundle')}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Container>
)
}