chore: heygermany customer project
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user