From 9a0c6c1f0b667c51dac2e984d4da21ff90b716f3 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 12:21:08 +0800 Subject: [PATCH 01/11] feat: implement file upload functionality and enhance admin dashboard layout --- app/apis/admin/get-users.ts | 2 - app/apis/admin/upload-file.ts | 37 +++++++ app/components/icons/chevron-double.tsx | 28 ------ app/components/ui/input-file.tsx | 12 ++- app/components/ui/pagination.tsx | 66 ------------- app/contexts/admin.tsx | 17 ++-- app/layouts/admin/dashboard.tsx | 3 +- app/layouts/admin/form-upload.tsx | 122 ++++++++++++++++++++++++ app/routes/actions.admin.upload.tsx | 64 +++++++++++++ 9 files changed, 246 insertions(+), 105 deletions(-) create mode 100644 app/apis/admin/upload-file.ts delete mode 100644 app/components/icons/chevron-double.tsx delete mode 100644 app/components/ui/pagination.tsx create mode 100644 app/layouts/admin/form-upload.tsx create mode 100644 app/routes/actions.admin.upload.tsx diff --git a/app/apis/admin/get-users.ts b/app/apis/admin/get-users.ts index 56d17e7..db411c4 100644 --- a/app/apis/admin/get-users.ts +++ b/app/apis/admin/get-users.ts @@ -26,9 +26,7 @@ const usersResponseSchema = z.object({ data: z.array(userResponseSchema), }) -export type TSubscribePlanRespon = z.infer export type TUserResponse = z.infer -export type TSubscribeResponse = z.infer export const getUsers = async (parameters: THttpServer) => { try { diff --git a/app/apis/admin/upload-file.ts b/app/apis/admin/upload-file.ts new file mode 100644 index 0000000..e94f376 --- /dev/null +++ b/app/apis/admin/upload-file.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' + +import type { TUploadSchema } from '~/layouts/admin/form-upload' +import { HttpServer, type THttpServer } from '~/libs/http-server' + +const uploadResponseSchema = z.object({ + data: z.object({ + message: z.string(), + data: z.object({ + file_path: z.string(), + file_url: z.string(), + }), + }), +}) + +type TParameter = { + payload: TUploadSchema & { + file: File + } +} & THttpServer + +export const uploadFileRequest = async (parameters: TParameter) => { + const { payload, ...restParameters } = parameters + const formdata = new FormData() + formdata.append('file', payload.file) + + try { + const { data } = await HttpServer(restParameters).post( + '/api/file', + formdata, + ) + return uploadResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/components/icons/chevron-double.tsx b/app/components/icons/chevron-double.tsx deleted file mode 100644 index db9cadf..0000000 --- a/app/components/icons/chevron-double.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { JSX, SVGProps } from 'react' - -/** - * Note: `ChevronDoubleIcon` default mengarah ke kiri. - * Gunakan class `rotate-xx` untuk mengubah arah ikon. - */ -export const ChevronDoubleIcon = ( - properties: JSX.IntrinsicAttributes & SVGProps, -) => { - return ( - - - - ) -} diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index b3570cc..e3ff8de 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -1,6 +1,6 @@ import { Field, Label, Input as HeadlessInput } from '@headlessui/react' import { CloudArrowUpIcon } from '@heroicons/react/20/solid' -import { type ComponentProps, type ReactNode } from 'react' +import { useEffect, type ComponentProps, type ReactNode } from 'react' import { get, type FieldError, @@ -42,15 +42,23 @@ export const InputFile = >( labelClassName, ...restProperties } = properties - const { setIsUploadOpen } = useAdminContext() + const { setIsUploadOpen, uploadedFile } = useAdminContext() const { register, formState: { errors }, + setValue, } = useRemixFormContext() const error: FieldError = get(errors, name) + useEffect(() => { + if (uploadedFile) { + setValue('featured_image', uploadedFile) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uploadedFile]) + return ( void -} - -export const Pagination: React.FC = ({ - currentPage = 1, - totalPages, - onPageChange, -}) => { - const renderPageNumbers = () => { - const pages = [] - for (let index = 1; index <= totalPages; index++) { - pages.push( - , - ) - } - return pages - } - - return ( -
- - - - {renderPageNumbers()} - - - -
- ) -} diff --git a/app/contexts/admin.tsx b/app/contexts/admin.tsx index 7e093a1..6f87995 100644 --- a/app/contexts/admin.tsx +++ b/app/contexts/admin.tsx @@ -6,17 +6,19 @@ import { type Dispatch, type SetStateAction, } from 'react' +import { z } from 'zod' -type TUpload = - | 'featured_image' - | 'ads' - | 'content' - | 'profile_picture' - | undefined +export const uploadCategorySchema = z + .enum(['featured_image', 'ads', 'content', 'profile_picture']) + .optional() + +type TUpload = z.infer type AdminContextProperties = { isUploadOpen: TUpload setIsUploadOpen: Dispatch> + uploadedFile?: string + setUploadedFile: Dispatch> } const AdminContext = createContext( @@ -25,12 +27,15 @@ const AdminContext = createContext( export const AdminProvider = ({ children }: PropsWithChildren) => { const [isUploadOpen, setIsUploadOpen] = useState() + const [uploadedFile, setUploadedFile] = useState() return ( {children} diff --git a/app/layouts/admin/dashboard.tsx b/app/layouts/admin/dashboard.tsx index 74e7858..642918b 100644 --- a/app/layouts/admin/dashboard.tsx +++ b/app/layouts/admin/dashboard.tsx @@ -3,6 +3,7 @@ import type { PropsWithChildren } from 'react' import { useAdminContext } from '~/contexts/admin' +import { FormUpload } from './form-upload' import { Navbar } from './navbar' import { Sidebar } from './sidebar' @@ -34,7 +35,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => { transition className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg" > - Upload di mari {isUploadOpen} + diff --git a/app/layouts/admin/form-upload.tsx b/app/layouts/admin/form-upload.tsx new file mode 100644 index 0000000..85b223a --- /dev/null +++ b/app/layouts/admin/form-upload.tsx @@ -0,0 +1,122 @@ +import { Button } from '@headlessui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect, useState, type ChangeEvent } from 'react' +import { useFetcher } from 'react-router' +import { RemixFormProvider, useRemixForm } from 'remix-hook-form' +import { z } from 'zod' + +import { uploadCategorySchema, useAdminContext } from '~/contexts/admin' + +export const uploadSchema = z.object({ + file: z.instanceof(File), + category: uploadCategorySchema, +}) + +export type TUploadSchema = z.infer + +export const FormUpload = () => { + const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext() + const fetcher = useFetcher() + const [disabled, setDisabled] = useState(false) + const [error, setError] = useState() + const maxFileSize = 1024 // 1MB + + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(uploadSchema), + }) + + const { handleSubmit, register, setValue } = formMethods + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + setDisabled(false) + return + } + + setUploadedFile(fetcher.data.uploadData.data.file_url) + setIsUploadOpen(undefined) + + setDisabled(true) + setError(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) + + const handleChange = async function (event: ChangeEvent) { + event.preventDefault() + if (event.target.files && event.target.files[0]) { + const files: File[] = [...event.target.files] + + onChange(files, event) + } + } + + const onChange = async function ( + files: File[], + event: ChangeEvent, + ) { + const file = files[0] + const img = new Image() + + if (!file.type.startsWith('image/')) { + setError('Please upload an image file.') + return + } + + if (file.size > maxFileSize * 1024) { + setError(`File size is too big!`) + return + } + + img.addEventListener('load', () => { + handleFiles(event) + }) + + img.src = URL.createObjectURL(file) + } + + const handleFiles = (event: ChangeEvent) => { + const files = event.target.files + if (files && files.length > 0) { + const file = files[0] + setValue('file', file) + } + } + + return ( + + + {error && ( +
{error}
+ )} + + + +
+
+ ) +} diff --git a/app/routes/actions.admin.upload.tsx b/app/routes/actions.admin.upload.tsx new file mode 100644 index 0000000..6faa5d9 --- /dev/null +++ b/app/routes/actions.admin.upload.tsx @@ -0,0 +1,64 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { uploadFileRequest } from '~/apis/admin/upload-file' +import { uploadSchema, type TUploadSchema } from '~/layouts/admin/form-upload' +import { handleCookie } from '~/libs/cookies' + +import type { Route } from './+types/actions.register' + +export const action = async ({ request }: Route.ActionArgs) => { + const { staffToken } = await handleCookie(request) + try { + const { + errors, + data: payload, + receivedValues: defaultValues, + } = await getValidatedFormData( + request, + zodResolver(uploadSchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: uploadData } = await uploadFileRequest({ + payload, + accessToken: staffToken, + }) + + return data( + { + success: true, + uploadData, + }, + { + status: 200, + statusText: 'OK', + }, + ) + } catch (error) { + if (error instanceof XiorError) { + return data( + { + success: false, + message: error?.response?.data?.error?.message || error.message, + }, + { + status: error?.response?.status || 500, + }, + ) + } + return data( + { + success: false, + message: 'Internal server error', + }, + { status: 500 }, + ) + } +} From aae10cf3bca929c45c2226c8635f24b36d2d1c5c Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 12:26:56 +0800 Subject: [PATCH 02/11] fix: update InputFile component to use dynamic field name for setting uploaded file --- app/components/ui/input-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index e3ff8de..3c19373 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -54,7 +54,7 @@ export const InputFile = >( useEffect(() => { if (uploadedFile) { - setValue('featured_image', uploadedFile) + setValue(name as string, uploadedFile) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedFile]) From 2e13e11673f2ff91cf65adc35f012b06ec4d7d6b Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 12:31:29 +0800 Subject: [PATCH 03/11] fix: add setUploadedFile to InputFile component for resetting uploaded file state --- app/components/ui/input-file.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index 3c19373..c43319e 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -42,7 +42,7 @@ export const InputFile = >( labelClassName, ...restProperties } = properties - const { setIsUploadOpen, uploadedFile } = useAdminContext() + const { setIsUploadOpen, uploadedFile, setUploadedFile } = useAdminContext() const { register, @@ -55,6 +55,7 @@ export const InputFile = >( useEffect(() => { if (uploadedFile) { setValue(name as string, uploadedFile) + setUploadedFile(undefined) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedFile]) From c70aa86be689d53491a3dbc5f89845e1547cad9c Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 12:34:43 +0800 Subject: [PATCH 04/11] fix: update admin menu to replace icon for Subscribe Plan with DocumentCurrencyDollarIcon --- app/layouts/admin/menu.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/layouts/admin/menu.ts b/app/layouts/admin/menu.ts index e898d93..1ca8692 100644 --- a/app/layouts/admin/menu.ts +++ b/app/layouts/admin/menu.ts @@ -1,4 +1,8 @@ -import { ClipboardDocumentCheckIcon, TagIcon } from '@heroicons/react/20/solid' +import { + ClipboardDocumentCheckIcon, + DocumentCurrencyDollarIcon, + TagIcon, +} from '@heroicons/react/20/solid' import type { SVGProps } from 'react' import { ChartIcon } from '~/components/icons/chart' @@ -62,7 +66,7 @@ export const MENU: TMenu[] = [ { title: 'Subscribe Plan', url: '/lg-admin/subscribe-plan', - icon: TagIcon, + icon: DocumentCurrencyDollarIcon, }, ], }, From ba83a6a0258e15cf800b18ec8a501898efdd2598 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 13:37:50 +0800 Subject: [PATCH 05/11] fix: update InputFile component to conditionally reset uploaded file state based on upload status --- app/components/text-editor/editor-menubar.tsx | 520 +++++++++--------- app/components/ui/input-file.tsx | 5 +- 2 files changed, 269 insertions(+), 256 deletions(-) diff --git a/app/components/text-editor/editor-menubar.tsx b/app/components/text-editor/editor-menubar.tsx index 8e3c31b..8a5c612 100644 --- a/app/components/text-editor/editor-menubar.tsx +++ b/app/components/text-editor/editor-menubar.tsx @@ -1,3 +1,4 @@ +import { Input } from '@headlessui/react' import { ArrowUturnLeftIcon, ArrowUturnRightIcon, @@ -6,6 +7,7 @@ import { Bars3Icon, Bars4Icon, BoldIcon, + CloudArrowUpIcon, CodeBracketIcon, DocumentTextIcon, H1Icon, @@ -27,9 +29,12 @@ import { useState, useRef, useCallback, + useEffect, } from 'react' import { HexColorInput, HexColorPicker } from 'react-colorful' +import { Button } from '~/components/ui/button' +import { useAdminContext } from '~/contexts/admin' import { useClickOutside } from '~/hooks/use-click-outside' import { isHexCompatible, rgbToHex } from '~/utils/color' @@ -49,10 +54,27 @@ export const EditorMenuBar = (properties: TProperties) => { // category, disabled = false, } = properties - // const [isOpenImage, setIsOpenImage] = useState(false) + const { setIsUploadOpen, uploadedFile, setUploadedFile } = useAdminContext() + const [isOpenImage, setIsOpenImage] = useState(false) + const [imageUrl, setImageUrl] = useState('') const [isOpenColor, setIsOpenColor] = useState(false) const popover = useRef(null) - const close = useCallback(() => setIsOpenColor(false), []) + const close = useCallback(() => { + setIsOpenColor(false) + setIsOpenImage(false) + if (imageUrl) { + addImage(imageUrl) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (uploadedFile) { + addImage(uploadedFile) + setUploadedFile(undefined) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uploadedFile]) useClickOutside(popover, close) @@ -96,167 +118,150 @@ export const EditorMenuBar = (properties: TProperties) => { } } - const uploadEnabled = false - const toggleUpload = () => { - if (uploadEnabled) { - // setIsOpenImage(true) - } else { - const urlImage = globalThis.prompt('URL') - if (urlImage) { - addImage(urlImage) - } - } - } - return ( - <> -
-
-
+
+
+
+ editor.chain().focus().toggleBold().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleBold().run() + } + isActive={editor.isActive('bold')} + title="Bold" + > + + + editor.chain().focus().toggleItalic().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleItalic().run() + } + isActive={editor.isActive('italic')} + title="Italic" + > + + + editor.chain().focus().toggleStrike().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleStrike().run() + } + isActive={editor.isActive('strike')} + title="Strike" + > + + +
editor.chain().focus().toggleBold().run()} - disabled={ - disabled || !editor.can().chain().focus().toggleBold().run() - } - isActive={editor.isActive('bold')} - title="Bold" + onClick={() => setIsOpenColor(true)} + title="Text Color" + style={{ + color: rgbColor, + }} + isActive={true} + disabled={disabled} > - + - editor.chain().focus().toggleItalic().run()} - disabled={ - disabled || !editor.can().chain().focus().toggleItalic().run() - } - isActive={editor.isActive('italic')} - title="Italic" - > - - - editor.chain().focus().toggleStrike().run()} - disabled={ - disabled || !editor.can().chain().focus().toggleStrike().run() - } - isActive={editor.isActive('strike')} - title="Strike" - > - - -
- setIsOpenColor(true)} - title="Text Color" - style={{ - color: rgbColor, - }} - isActive={true} - disabled={disabled} + {isOpenColor && ( +
- - - {isOpenColor && ( -
- +
+ -
- -
- )} -
- editor.chain().focus().setTextAlign('left').run()} - disabled={ - disabled || - !editor.can().chain().focus().setTextAlign('left').run() - } - isActive={editor.isActive({ textAlign: 'left' })} - title="Align Left" - > - - - - editor.chain().focus().setTextAlign('center').run() - } - disabled={ - disabled || - !editor.can().chain().focus().setTextAlign('center').run() - } - isActive={editor.isActive({ textAlign: 'center' })} - title="Align Center" - > - - - editor.chain().focus().setTextAlign('right').run()} - disabled={ - disabled || - !editor.can().chain().focus().setTextAlign('right').run() - } - isActive={editor.isActive({ textAlign: 'right' })} - title="Align Right" - > - - - - editor.chain().focus().setTextAlign('justify').run() - } - disabled={ - disabled || - !editor.can().chain().focus().setTextAlign('justify').run() - } - isActive={editor.isActive({ textAlign: 'justify' })} - title="Align Justify" - > - - +
+ )}
-
- - editor.chain().focus().toggleHeading({ level: 1 }).run() - } - isActive={editor.isActive('heading', { level: 1 })} - title="Heading 1" - disabled={disabled} - > - - - - editor.chain().focus().toggleHeading({ level: 2 }).run() - } - isActive={editor.isActive('heading', { level: 2 })} - title="Heading 2" - disabled={disabled} - > - - - - editor.chain().focus().toggleHeading({ level: 3 }).run() - } - isActive={editor.isActive('heading', { level: 3 })} - title="Heading 3" - disabled={disabled} - > - - - {/* editor.chain().focus().setTextAlign('left').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('left').run() + } + isActive={editor.isActive({ textAlign: 'left' })} + title="Align Left" + > + + + editor.chain().focus().setTextAlign('center').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('center').run() + } + isActive={editor.isActive({ textAlign: 'center' })} + title="Align Center" + > + + + editor.chain().focus().setTextAlign('right').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('right').run() + } + isActive={editor.isActive({ textAlign: 'right' })} + title="Align Right" + > + + + editor.chain().focus().setTextAlign('justify').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('justify').run() + } + isActive={editor.isActive({ textAlign: 'justify' })} + title="Align Justify" + > + + +
+
+ + editor.chain().focus().toggleHeading({ level: 1 }).run() + } + isActive={editor.isActive('heading', { level: 1 })} + title="Heading 1" + disabled={disabled} + > + + + + editor.chain().focus().toggleHeading({ level: 2 }).run() + } + isActive={editor.isActive('heading', { level: 2 })} + title="Heading 2" + disabled={disabled} + > + + + + editor.chain().focus().toggleHeading({ level: 3 }).run() + } + isActive={editor.isActive('heading', { level: 3 })} + title="Heading 3" + disabled={disabled} + > + + + {/* editor.chain().focus().setParagraph().run()} isActive={editor.isActive('paragraph')} title="Paragraph" @@ -264,32 +269,32 @@ export const EditorMenuBar = (properties: TProperties) => { > */} - editor.chain().focus().toggleBulletList().run()} - isActive={editor.isActive('bulletList')} - title="Bullet List" - disabled={disabled} - > - - - editor.chain().focus().toggleOrderedList().run()} - isActive={editor.isActive('orderedList')} - title="Ordered List" - disabled={disabled} - > - - - editor.chain().focus().toggleCodeBlock().run()} - isActive={editor.isActive('codeBlock')} - title="Code Block" - disabled={disabled} - > - - -
- {/*
+ editor.chain().focus().toggleBulletList().run()} + isActive={editor.isActive('bulletList')} + title="Bullet List" + disabled={disabled} + > + + + editor.chain().focus().toggleOrderedList().run()} + isActive={editor.isActive('orderedList')} + title="Ordered List" + disabled={disabled} + > + + + editor.chain().focus().toggleCodeBlock().run()} + isActive={editor.isActive('codeBlock')} + title="Code Block" + disabled={disabled} + > + + +
+ {/*
editor.chain().focus().toggleBlockquote().run()} isActive={editor.isActive('blockquote')} @@ -306,7 +311,7 @@ export const EditorMenuBar = (properties: TProperties) => {
*/} - {/*
+ {/*
editor.chain().focus().setHardBreak().run()} title="Hard Break" @@ -325,89 +330,96 @@ export const EditorMenuBar = (properties: TProperties) => {
*/} -
+
+
toggleUpload()} + onClick={() => setIsOpenImage(true)} title="Insert Image" disabled={disabled} > - setLink()} - disabled={ - disabled || - !editor - .can() - .chain() - .focus() - .extendMarkRange('link') - .setLink({ href: '' }) - .run() - } - isActive={editor.isActive('link')} - title="Set Link" - > - - - editor.chain().focus().unsetLink().run()} - disabled={disabled || !editor.isActive('link')} - title="Unset Link" - > - - -
-
- editor.chain().focus().undo().run()} - disabled={disabled || !editor.can().chain().focus().undo().run()} - title="Undo" - > - - - editor.chain().focus().redo().run()} - disabled={disabled || !editor.can().chain().focus().redo().run()} - title="Redo" - > - - + {isOpenImage && ( +
+
+ { + setImageUrl(event.target.value) + }} + className="z-10 flex h-[42px] w-full rounded-lg border-0 bg-white p-2 pr-8 text-sm shadow read-only:bg-gray-100 focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none focus-visible:outline-0 disabled:bg-gray-100" + /> + +
+
+ )}
+ setLink()} + disabled={ + disabled || + !editor + .can() + .chain() + .focus() + .extendMarkRange('link') + .setLink({ href: '' }) + .run() + } + isActive={editor.isActive('link')} + title="Set Link" + > + + + editor.chain().focus().unsetLink().run()} + disabled={disabled || !editor.isActive('link')} + title="Unset Link" + > + +
-
-
- setIsPlainHTML(true)} - title="Switch to Plain Text" - > - - -
+
+ editor.chain().focus().undo().run()} + disabled={disabled || !editor.can().chain().focus().undo().run()} + title="Undo" + > + + + editor.chain().focus().redo().run()} + disabled={disabled || !editor.can().chain().focus().redo().run()} + title="Redo" + > + +
- - {/* - setIsOpenImage(false), - onSave: (file) => { - addImage(file) - setIsOpenImage(false) - }, - category: category, - maxFileSize: 300, - selectedFile: '', - }} - > - - - */} - +
+
+ setIsPlainHTML(true)} + title="Switch to Plain Text" + > + + +
+
+
) } diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index c43319e..dc85361 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -42,7 +42,8 @@ export const InputFile = >( labelClassName, ...restProperties } = properties - const { setIsUploadOpen, uploadedFile, setUploadedFile } = useAdminContext() + const { setIsUploadOpen, uploadedFile, setUploadedFile, isUploadOpen } = + useAdminContext() const { register, @@ -53,7 +54,7 @@ export const InputFile = >( const error: FieldError = get(errors, name) useEffect(() => { - if (uploadedFile) { + if (uploadedFile && isUploadOpen === name) { setValue(name as string, uploadedFile) setUploadedFile(undefined) } From e93c0a846441972fe473a48ec2c02de9c362c38a Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 13:39:03 +0800 Subject: [PATCH 06/11] fix: increase maximum file size limit from 1MB to 10MB in FormUpload component --- app/layouts/admin/form-upload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layouts/admin/form-upload.tsx b/app/layouts/admin/form-upload.tsx index 85b223a..bd813b1 100644 --- a/app/layouts/admin/form-upload.tsx +++ b/app/layouts/admin/form-upload.tsx @@ -19,7 +19,7 @@ export const FormUpload = () => { const fetcher = useFetcher() const [disabled, setDisabled] = useState(false) const [error, setError] = useState() - const maxFileSize = 1024 // 1MB + const maxFileSize = 10 * 1024 // 10MB const formMethods = useRemixForm({ mode: 'onSubmit', From 169f0166b83a0815ed443b4b45167455936831e3 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 14:32:32 +0800 Subject: [PATCH 07/11] fix: update upload handling to conditionally reset upload state based on category --- app/components/text-editor/editor-menubar.tsx | 13 +++++-------- app/components/ui/input-file.tsx | 5 +++-- app/contexts/admin.tsx | 2 +- app/layouts/admin/form-upload.tsx | 3 +-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/components/text-editor/editor-menubar.tsx b/app/components/text-editor/editor-menubar.tsx index 8a5c612..e1ce6ae 100644 --- a/app/components/text-editor/editor-menubar.tsx +++ b/app/components/text-editor/editor-menubar.tsx @@ -48,13 +48,9 @@ type TProperties = { } export const EditorMenuBar = (properties: TProperties) => { - const { - editor, - setIsPlainHTML, - // category, - disabled = false, - } = properties - const { setIsUploadOpen, uploadedFile, setUploadedFile } = useAdminContext() + const { editor, setIsPlainHTML, category, disabled = false } = properties + const { setIsUploadOpen, uploadedFile, setUploadedFile, isUploadOpen } = + useAdminContext() const [isOpenImage, setIsOpenImage] = useState(false) const [imageUrl, setImageUrl] = useState('') const [isOpenColor, setIsOpenColor] = useState(false) @@ -69,9 +65,10 @@ export const EditorMenuBar = (properties: TProperties) => { }, []) useEffect(() => { - if (uploadedFile) { + if (uploadedFile && isUploadOpen === category) { addImage(uploadedFile) setUploadedFile(undefined) + setIsUploadOpen(undefined) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedFile]) diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index dc85361..29d6f00 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -11,7 +11,7 @@ import { import { useRemixFormContext } from 'remix-hook-form' import { twMerge } from 'tailwind-merge' -import { useAdminContext } from '~/contexts/admin' +import { useAdminContext, type TUpload } from '~/contexts/admin' import { Button } from './button' @@ -57,6 +57,7 @@ export const InputFile = >( if (uploadedFile && isUploadOpen === name) { setValue(name as string, uploadedFile) setUploadedFile(undefined) + setIsUploadOpen(undefined) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedFile]) @@ -85,7 +86,7 @@ export const InputFile = >( size="fit" className="absolute right-3 h-[42px]" onClick={() => { - setIsUploadOpen('featured_image') + setIsUploadOpen(name as TUpload) }} > diff --git a/app/contexts/admin.tsx b/app/contexts/admin.tsx index 6f87995..c64ccc9 100644 --- a/app/contexts/admin.tsx +++ b/app/contexts/admin.tsx @@ -12,7 +12,7 @@ export const uploadCategorySchema = z .enum(['featured_image', 'ads', 'content', 'profile_picture']) .optional() -type TUpload = z.infer +export type TUpload = z.infer type AdminContextProperties = { isUploadOpen: TUpload diff --git a/app/layouts/admin/form-upload.tsx b/app/layouts/admin/form-upload.tsx index bd813b1..3ee6801 100644 --- a/app/layouts/admin/form-upload.tsx +++ b/app/layouts/admin/form-upload.tsx @@ -15,7 +15,7 @@ export const uploadSchema = z.object({ export type TUploadSchema = z.infer export const FormUpload = () => { - const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext() + const { isUploadOpen, setUploadedFile } = useAdminContext() const fetcher = useFetcher() const [disabled, setDisabled] = useState(false) const [error, setError] = useState() @@ -37,7 +37,6 @@ export const FormUpload = () => { } setUploadedFile(fetcher.data.uploadData.data.file_url) - setIsUploadOpen(undefined) setDisabled(true) setError(undefined) From 54a8a97c099ee63983417a52db7c62e0c0ccc0fe Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 10 Mar 2025 14:49:09 +0800 Subject: [PATCH 08/11] fix: encode URI components in API calls and links to ensure proper formatting --- app/apis/common/get-news-by-slug.ts | 4 +++- app/pages/dashboard-contents/index.tsx | 2 +- app/utils/render.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/apis/common/get-news-by-slug.ts b/app/apis/common/get-news-by-slug.ts index a46f417..72c4c6d 100644 --- a/app/apis/common/get-news-by-slug.ts +++ b/app/apis/common/get-news-by-slug.ts @@ -14,7 +14,9 @@ type TParameters = { export const getNewsBySlug = async (parameters: TParameters) => { const { slug, ...restParameters } = parameters try { - const { data } = await HttpServer(restParameters).get(`/api/news/${slug}`) + const { data } = await HttpServer(restParameters).get( + `/api/news/${encodeURIComponent(slug)}`, + ) return dataResponseSchema.parse(data) } catch (error) { // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject diff --git a/app/pages/dashboard-contents/index.tsx b/app/pages/dashboard-contents/index.tsx index fb13630..487ac0c 100644 --- a/app/pages/dashboard-contents/index.tsx +++ b/app/pages/dashboard-contents/index.tsx @@ -83,7 +83,7 @@ export const ContentsPage = () => { 7: (value: string) => (