feat: add create article form and related components

This commit is contained in:
2026-04-01 14:24:13 -03:00
parent a3ea9e8dc8
commit 9c0006e2dc
16 changed files with 815 additions and 10 deletions

View File

@@ -0,0 +1,197 @@
'use client';
import { saveArticle } from '@/lib/feature/article/article.external';
import { Button } from '@/ui/components/shadcn/button';
import {
Field,
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 { useEffect } 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 formSchema = z.object({
title: z.string().min(3).max(255),
slug: z.string().min(3),
description: z.string().min(10),
coverImageUrl: z.string().url(),
content: z.string().min(10),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
slug: '',
description: '',
coverImageUrl: '',
content: '',
},
});
const title = useWatch({
control: form.control,
name: 'title',
});
useEffect(() => {
if (!title) return;
form.setValue('slug', slugify(title).toLowerCase());
}, [form, title]);
async function onSubmit(data: z.infer<typeof formSchema>) {
try {
const result = await saveArticle(data);
toast.success('Article created successfully!', {
description: `Article "${result.title}" has been created.`,
position: 'bottom-right',
});
form.reset();
} catch (error) {
toast.error('Failed to create article', {
description:
error instanceof Error
? error.message
: 'An error occurred',
position: 'bottom-right',
});
}
}
return (
<form id='form-create-article' onSubmit={form.handleSubmit(onSubmit)}>
<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
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id='form-create-article-content'
placeholder='Write your article content here...'
rows={12}
className='min-h-48 resize-none font-mono text-sm'
aria-invalid={fieldState.invalid}
/>
</InputGroup>
{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;