From fb8c07d32e3faa5af172d04c9158e2ed8ef34fcd Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Fri, 10 Apr 2026 00:07:11 -0300 Subject: [PATCH] feat: add file upload section for cover image and article content in create article form --- plan-fileUploadField.prompt.md | 22 -------- public/img/icons/cover-image.svg | 1 + public/img/icons/markdown-content.svg | 1 + .../internal/create-article-form.tsx | 50 ++++++++----------- 4 files changed, 24 insertions(+), 50 deletions(-) delete mode 100644 plan-fileUploadField.prompt.md create mode 100644 public/img/icons/cover-image.svg create mode 100644 public/img/icons/markdown-content.svg diff --git a/plan-fileUploadField.prompt.md b/plan-fileUploadField.prompt.md deleted file mode 100644 index a849162..0000000 --- a/plan-fileUploadField.prompt.md +++ /dev/null @@ -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 `` block in `create-article-form.tsx` with two `` 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`. - diff --git a/public/img/icons/cover-image.svg b/public/img/icons/cover-image.svg new file mode 100644 index 0000000..62df395 --- /dev/null +++ b/public/img/icons/cover-image.svg @@ -0,0 +1 @@ + diff --git a/public/img/icons/markdown-content.svg b/public/img/icons/markdown-content.svg new file mode 100644 index 0000000..6cd6308 --- /dev/null +++ b/public/img/icons/markdown-content.svg @@ -0,0 +1 @@ + diff --git a/src/ui/components/internal/create-article-form.tsx b/src/ui/components/internal/create-article-form.tsx index 189d6e7..433105a 100644 --- a/src/ui/components/internal/create-article-form.tsx +++ b/src/ui/components/internal/create-article-form.tsx @@ -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 (
@@ -239,19 +247,12 @@ export const CreateArticleForm = () => { description='PNG, JPG, GIF, WebP accepted' error={form.formState.errors.coverImageUrl?.message} icon={ - - - + } /> { description='.md / .markdown / .txt accepted' error={form.formState.errors.content?.message} icon={ - - - + } />