Adds frontend for cms page

This commit is contained in:
2026-06-30 23:23:50 +02:00
parent c156107cb1
commit 488ab821a7
38 changed files with 2047 additions and 39 deletions
@@ -0,0 +1,138 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { PasswordField } from '@/components/ui/PasswordField';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { FormErrorBanner } from '@/components/ui/FormErrorBanner';
import { FieldError } from '@/components/ui/FieldError';
import { useAddCmsInstance } from '@/api/useAddCmsInstance';
import { NetworkError, ProblemDetailsError } from '@/lib/api-client';
import { addCmsInstanceSchema, type AddCmsInstanceFormData } from '@/lib/schemas/cms';
interface AddCmsInstanceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function AddCmsInstanceDialog({ open, onOpenChange }: AddCmsInstanceDialogProps) {
const { t } = useTranslation();
const [serverError, setServerError] = useState<string | null>(null);
const addInstance = useAddCmsInstance({
onError: (err) => {
if (err instanceof ProblemDetailsError && err.status === 409) {
setServerError(t('cms.add.errors.conflict'));
} else if (err instanceof NetworkError) {
setServerError(t('errors.network'));
} else {
setServerError(t('errors.generic'));
}
},
});
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<AddCmsInstanceFormData>({
resolver: zodResolver(addCmsInstanceSchema),
mode: 'onTouched',
});
useEffect(() => {
if (!open) {
setServerError(null);
addInstance.reset();
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
const onSubmit = handleSubmit(async (values) => {
setServerError(null);
try {
await addInstance.mutateAsync(values);
toast.success(t('cms.add.successToast'));
onOpenChange(false);
} catch {
// error shown via serverError state
}
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('cms.add.title')}</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit} noValidate className="space-y-4">
<FormErrorBanner
error={serverError !== null ? { message: serverError } : null}
onDismiss={() => setServerError(null)}
/>
<div className="space-y-2">
<Label htmlFor="add-cms-name">{t('cms.add.nameLabel')}</Label>
<Input
id="add-cms-name"
type="text"
data-testid="add-cms-name"
aria-invalid={errors.name !== undefined}
{...register('name')}
/>
{errors.name && (
<FieldError message={errors.name.message} testId="add-cms-name-error" />
)}
</div>
<div className="space-y-2">
<Label htmlFor="add-cms-url">{t('cms.add.urlLabel')}</Label>
<Input
id="add-cms-url"
type="text"
placeholder="http://cms.example.com"
data-testid="add-cms-url"
aria-invalid={errors.url !== undefined}
{...register('url')}
/>
{errors.url && (
<FieldError message={errors.url.message} testId="add-cms-url-error" />
)}
</div>
<div className="space-y-2">
<Label htmlFor="add-cms-apikey">{t('cms.add.apiKeyLabel')}</Label>
<PasswordField
id="add-cms-apikey"
{...register('apiKey')}
/>
{errors.apiKey && (
<FieldError message={errors.apiKey.message} testId="add-cms-apikey-error" />
)}
</div>
<Button
type="submit"
className="w-full"
disabled={isSubmitting || addInstance.isPending}
data-testid="add-cms-submit"
>
{isSubmitting || addInstance.isPending ? '…' : t('cms.add.submitButton')}
</Button>
</form>
</DialogContent>
</Dialog>
);
}