Files
hideyoshi-blog/src/ui/components/internal/create-article-form.tsx
Vitor Hideyoshi 3f9613d98b
Some checks failed
Build and Test / run-test (20.x) (push) Failing after 1m36s
WIP
2026-04-09 20:57:35 -03:00

221 lines
8.5 KiB
TypeScript

'use client';
import { saveArticle } from '@/lib/feature/article/article.external';
import { Button } from '@/ui/components/shadcn/button';
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/ui/components/shadcn/field';
import { Input } from '@/ui/components/shadcn/input';
import {
InputGroup,
InputGroupTextarea,
} from '@/ui/components/shadcn/input-group';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useEffect, useRef } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import slugify from 'slugify';
import { toast } from 'sonner';
import { z } from 'zod';
export const CreateArticleForm = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const formSchema = z.object({
title: z.string().min(3).max(255),
slug: z.string().min(3),
description: z.string().min(10),
coverImageUrl: z.url(),
content: z.instanceof(File),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
slug: '',
description: '',
coverImageUrl: '',
content: undefined,
},
});
const title = useWatch({
control: form.control,
name: 'title',
});
useEffect(() => {
if (!title) return;
form.setValue('slug', slugify(title).toLowerCase());
}, [form, title]);
const handleFormSubmit = useCallback(
async (data: z.infer<typeof formSchema>) => {
try {
const result = await saveArticle({
...data,
content: await data.content.text(),
});
toast.success('Article created successfully!', {
description: `Article "${result.title}" has been created.`,
position: 'bottom-right',
});
form.reset();
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} catch (error) {
toast.error('Failed to create article', {
description:
error instanceof Error
? error.message
: 'An error occurred',
position: 'bottom-right',
});
}
},
[form]
);
return (
<form
id='form-create-article'
// eslint-disable-next-line react-hooks/refs
onSubmit={form.handleSubmit(handleFormSubmit)}
>
<FieldGroup>
<Controller
name='title'
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-title'>
Title
</FieldLabel>
<Input
{...field}
id='form-create-article-title'
aria-invalid={fieldState.invalid}
placeholder='Article title'
autoComplete='off'
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name='slug'
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-slug'>
Slug
</FieldLabel>
<Input
{...field}
id='form-create-article-slug'
aria-invalid={fieldState.invalid}
placeholder='article-slug'
autoComplete='off'
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name='description'
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-description'>
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id='form-create-article-description'
placeholder='A simple but nice description of the article here.'
rows={3}
className='min-h-24 resize-none'
aria-invalid={fieldState.invalid}
/>
</InputGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name='coverImageUrl'
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-cover-image-url'>
Cover Image URL
</FieldLabel>
<Input
{...field}
id='form-create-article-cover-image-url'
aria-invalid={fieldState.invalid}
placeholder='https://example.com/image.jpg'
type='url'
autoComplete='off'
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name='content'
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor='form-create-article-content'>
Content (Markdown File)
</FieldLabel>
<Input
ref={fileInputRef}
id='form-create-article-content'
type='file'
accept='.md,.markdown'
aria-invalid={fieldState.invalid}
onChange={(event) =>
field.onChange(
event.target.files &&
event.target.files[0]
)
}
/>
<FieldDescription>
Select your article.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
<div className='flex w-full justify-end'>
<Button type='submit' className='mt-6'>
Create Article
</Button>
</div>
</form>
);
};
export default CreateArticleForm;