feat: add create article form and related components
This commit is contained in:
197
src/ui/components/internal/create-article-form.tsx
Normal file
197
src/ui/components/internal/create-article-form.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user