feat: add file upload section for cover image and article content in create article form
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
# Plan: Refactor FileUploadSection into generic FileUploadField
|
||||
|
||||
Replace the composite, internally-coupled `FileUploadSection` with a generic, controlled `FileUploadField` component. Business logic (validation helpers, toast errors, URL generation) moves up into `CreateArticleForm`, and two `FileUploadField` instances replace the single `FileUploadSection`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `file-upload-field.tsx` as a controlled `FileUploadField` component accepting `file: File | null`, `onFileChange: (file: File | null) => void`, `accept`, `validate`, `label`, `description`, and `error` props — rendering the `FileUpload`/`Field` UI shell with no internal business logic.
|
||||
|
||||
2. Move `isImageFile`, `isContentFile`, `generateFakeImageUrl`, and rejection-toast handlers into `create-article-form.tsx`, adding `coverImageFile` and `contentFile` state entries (or keeping them as `useForm` values directly).
|
||||
|
||||
3. Replace the `<FileUploadSection>` block in `create-article-form.tsx` with two `<FileUploadField>` instances (one for cover image, one for content), wiring each one to the form state via `Controller` or direct `setValue`.
|
||||
|
||||
4. Delete `file-upload-section.tsx` now that it has no consumers.
|
||||
|
||||
## Further Considerations
|
||||
|
||||
1. **`onFileChange` vs `setFile`** — `onFileChange` follows idiomatic React event-prop naming; `setFile` reads more like a state-setter leak. `onFileChange` is recommended but either works.
|
||||
|
||||
2. **State ownership for files** — Cover image needs a `File` reference for URL revocation cleanup; should this live in a `useState` inside the form or in the `FileUploadField` itself via a ref? Keeping it inside the form is cleaner for reset logic.
|
||||
|
||||
3. **Null on clear** — The `onFileChange(null)` case (user deletes the file) should also clear the related form field value (`coverImageUrl` → `''`, `content` → `''`), so the handler in the form needs to handle both `File` and `null`.
|
||||
|
||||
1
public/img/icons/cover-image.svg
Normal file
1
public/img/icons/cover-image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="#4B5563" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m4 16 4.586-4.586a2 2 0 0 1 2.828 0L16 16m-2-2 1.586-1.586a2 2 0 0 1 2.828 0L20 14m-6-6h.01M6 20h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2" class="dark:stroke-[#9CA3AF]"/></svg>
|
||||
|
After Width: | Height: | Size: 364 B |
1
public/img/icons/markdown-content.svg
Normal file
1
public/img/icons/markdown-content.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="#4B5563" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 0 0 2-2V9.414a1 1 0 0 0-.293-.707l-5.414-5.414A1 1 0 0 0 12.586 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2" class="dark:stroke-[#9CA3AF]"/></svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -15,11 +15,14 @@ import {
|
||||
InputGroupTextarea,
|
||||
} from '@/ui/components/shadcn/input-group';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Image from 'next/image';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import slugify from 'slugify';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
import ImageLogo from '~/public/img/icons/cover-image.svg';
|
||||
import MarkdownLogo from '~/public/img/icons/markdown-content.svg';
|
||||
|
||||
function isImageFile(file: File): boolean {
|
||||
return file.type.startsWith('image/');
|
||||
@@ -37,11 +40,15 @@ function isContentFile(file: File): boolean {
|
||||
}
|
||||
|
||||
function validateImageFile(file: File): string | null {
|
||||
return isImageFile(file) ? null : 'Only image files are allowed for cover image';
|
||||
return isImageFile(file)
|
||||
? null
|
||||
: 'Only image files are allowed for cover image';
|
||||
}
|
||||
|
||||
function validateContentFile(file: File): string | null {
|
||||
return isContentFile(file) ? null : 'Only markdown or text files are allowed';
|
||||
return isContentFile(file)
|
||||
? null
|
||||
: 'Only markdown or text files are allowed';
|
||||
}
|
||||
|
||||
export const CreateArticleForm = () => {
|
||||
@@ -159,6 +166,7 @@ export const CreateArticleForm = () => {
|
||||
return (
|
||||
<form
|
||||
id='form-create-article'
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
onSubmit={form.handleSubmit(handleFormSubmit)}
|
||||
>
|
||||
<FieldGroup className='gap-7'>
|
||||
@@ -239,19 +247,12 @@ export const CreateArticleForm = () => {
|
||||
description='PNG, JPG, GIF, WebP accepted'
|
||||
error={form.formState.errors.coverImageUrl?.message}
|
||||
icon={
|
||||
<svg
|
||||
className='size-6 shrink-0 text-muted-foreground'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
|
||||
/>
|
||||
</svg>
|
||||
<Image
|
||||
src={ImageLogo}
|
||||
alt=''
|
||||
aria-hidden='true'
|
||||
className='size-6 shrink-0 opacity-60'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FileUploadField
|
||||
@@ -264,19 +265,12 @@ export const CreateArticleForm = () => {
|
||||
description='.md / .markdown / .txt accepted'
|
||||
error={form.formState.errors.content?.message}
|
||||
icon={
|
||||
<svg
|
||||
className='size-6 shrink-0 text-muted-foreground'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'
|
||||
/>
|
||||
</svg>
|
||||
<Image
|
||||
src={MarkdownLogo}
|
||||
alt=''
|
||||
aria-hidden='true'
|
||||
className='size-6 shrink-0 opacity-60'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user