diff --git a/.vscode/settings.json b/.vscode/settings.json index 990a0ee..d57e2f3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,8 @@ "labelClassName", "buttonClassName", "leftNodeClassName", - "rightNodeClassName" + "rightNodeClassName", + "buttonVariants", + "cva" ] } diff --git a/app/apis/admin/upload-file.ts b/app/apis/admin/upload-file.ts index e94f376..f96471a 100644 --- a/app/apis/admin/upload-file.ts +++ b/app/apis/admin/upload-file.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -import type { TUploadSchema } from '~/layouts/admin/form-upload' +import type { TUploadSchema } from '~/layouts/admin/dialog-upload' import { HttpServer, type THttpServer } from '~/libs/http-server' const uploadResponseSchema = z.object({ diff --git a/app/components/text-editor/editor-button.tsx b/app/components/text-editor/editor-button.tsx index e71113f..7e62f94 100644 --- a/app/components/text-editor/editor-button.tsx +++ b/app/components/text-editor/editor-button.tsx @@ -1,3 +1,4 @@ +import { Button } from '@headlessui/react' import type { CSSProperties, MouseEventHandler, ReactNode } from 'react' import { twMerge } from 'tailwind-merge' @@ -16,12 +17,12 @@ export const EditorButton = (properties: TProperties) => { properties return ( - + ) } diff --git a/app/components/text-editor/editor-menubar.tsx b/app/components/text-editor/editor-menubar.tsx index 09395f2..34ba522 100644 --- a/app/components/text-editor/editor-menubar.tsx +++ b/app/components/text-editor/editor-menubar.tsx @@ -127,7 +127,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive('bold')} title="Bold" > - + editor.chain().focus().toggleItalic().run()} @@ -137,7 +137,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive('italic')} title="Italic" > - + editor.chain().focus().toggleStrike().run()} @@ -147,7 +147,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive('strike')} title="Strike" > - +
{ isActive={true} disabled={disabled} > - + {isOpenColor && (
{ isActive={editor.isActive({ textAlign: 'left' })} title="Align Left" > - + editor.chain().focus().setTextAlign('center').run()} @@ -202,7 +202,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive({ textAlign: 'center' })} title="Align Center" > - + editor.chain().focus().setTextAlign('right').run()} @@ -213,7 +213,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive({ textAlign: 'right' })} title="Align Right" > - + editor.chain().focus().setTextAlign('justify').run()} @@ -224,7 +224,7 @@ export const EditorMenuBar = (properties: TProperties) => { isActive={editor.isActive({ textAlign: 'justify' })} title="Align Justify" > - +
@@ -236,7 +236,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Heading 1" disabled={disabled} > - + @@ -246,7 +246,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Heading 2" disabled={disabled} > - + @@ -256,7 +256,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Heading 3" disabled={disabled} > - + {/* editor.chain().focus().setParagraph().run()} @@ -272,7 +272,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Bullet List" disabled={disabled} > - + editor.chain().focus().toggleOrderedList().run()} @@ -280,7 +280,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Ordered List" disabled={disabled} > - + editor.chain().focus().toggleCodeBlock().run()} @@ -288,7 +288,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Code Block" disabled={disabled} > - +
{/*
@@ -334,7 +334,7 @@ export const EditorMenuBar = (properties: TProperties) => { title="Insert Image" disabled={disabled} > - + {isOpenImage && (
{ isActive={editor.isActive('link')} title="Set Link" > - + editor.chain().focus().unsetLink().run()} disabled={disabled || !editor.isActive('link')} title="Unset Link" > - +
@@ -396,14 +396,14 @@ export const EditorMenuBar = (properties: TProperties) => { 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" > - +
@@ -413,7 +413,7 @@ export const EditorMenuBar = (properties: TProperties) => { onClick={() => setIsPlainHTML(true)} title="Switch to Plain Text" > - +
diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index f2f9eba..798fa4f 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -1,4 +1,5 @@ import { Button as HeadlessButton } from '@headlessui/react' +import { ArrowPathIcon } from '@heroicons/react/20/solid' import { cva, type VariantProps } from 'class-variance-authority' import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react' import { twMerge } from 'tailwind-merge' @@ -8,11 +9,14 @@ const buttonVariants = cva( { variants: { variant: { - newsPrimary: 'bg-[#2E2F7C] text-white text-lg', - newsPrimaryOutline: 'border-[3px] border-white text-white text-lg', - newsSecondary: 'border-[3px] border-[#2E2F7C] text-[#2E2F7C] text-lg', + newsPrimary: + 'bg-[#2E2F7C] text-white text-lg hover:bg-[#4C5CA0] hover:shadow transition active:bg-[#6970B4]', + newsPrimaryOutline: + 'border-[3px] bg-[#2E2F7C] border-white text-white text-lg hover:bg-[#4C5CA0] hover:shadow-lg active:shadow-2xl transition active:bg-[#6970B4]', + newsSecondary: + 'border-[3px] bg-white hover:shadow-lg active:shadow-2xl border-[#2E2F7C] text-[#2E2F7C] hover:text-[#4C5CA0] active:text-[#6970B4] text-lg hover:border-[#4C5CA0] transition active:border-[#6970B4]', icon: '', - link: '', + link: 'font-semibold text-[#2E2F7C] hover:text-[#4C5CA0] active:text-[#6970B4] transition', }, size: { default: 'h-[50px] w-[150px]', @@ -35,6 +39,7 @@ type ButtonBaseProperties = { variant?: VariantProps['variant'] size?: VariantProps['size'] className?: string + isLoading?: boolean } type PolymorphicReference = @@ -45,22 +50,27 @@ type ButtonProperties = ButtonBaseProperties & { ref?: PolymorphicReference } & Omit, keyof ButtonBaseProperties> -export const Button = ({ - as, - children, - variant, - size, - className, - ...properties -}: ButtonProperties) => { +export const Button = ( + properties: ButtonProperties, +) => { + const { + as, + children, + variant, + size, + className, + isLoading = false, + ...restProperties + } = properties const Component = as || HeadlessButton const classes = twMerge(buttonVariants({ variant, size, className })) return ( + {isLoading && } {children} ) diff --git a/app/layouts/admin/dashboard.tsx b/app/layouts/admin/dashboard.tsx index 642918b..8c9c3a4 100644 --- a/app/layouts/admin/dashboard.tsx +++ b/app/layouts/admin/dashboard.tsx @@ -1,15 +1,11 @@ -import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react' import type { PropsWithChildren } from 'react' -import { useAdminContext } from '~/contexts/admin' - -import { FormUpload } from './form-upload' +import { DialogUpload } from './dialog-upload' import { Navbar } from './navbar' import { Sidebar } from './sidebar' export const AdminDashboardLayout = (properties: PropsWithChildren) => { const { children } = properties - const { isUploadOpen, setIsUploadOpen } = useAdminContext() return (
@@ -18,27 +14,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
{children}
- { - setIsUploadOpen(undefined) - }} - className="relative z-50" - transition - > - -
- - - -
-
+ ) } diff --git a/app/layouts/admin/dialog-upload.tsx b/app/layouts/admin/dialog-upload.tsx new file mode 100644 index 0000000..2ff72c0 --- /dev/null +++ b/app/layouts/admin/dialog-upload.tsx @@ -0,0 +1,143 @@ +import { Dialog, DialogBackdrop, DialogPanel, Input } 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 { Button } from '~/components/ui/button' +import { uploadCategorySchema, useAdminContext } from '~/contexts/admin' + +export const uploadSchema = z.object({ + file: z.instanceof(File), + category: uploadCategorySchema, +}) + +export type TUploadSchema = z.infer + +export const DialogUpload = () => { + const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext() + const fetcher = useFetcher() + const [error, setError] = useState() + const maxFileSize = 10 * 1024 // 10MB + + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(uploadSchema), + }) + + const { handleSubmit, register, setValue } = formMethods + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + return + } + + setUploadedFile(fetcher.data.uploadData.data.file_url) + + 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 ( + { + if (fetcher.state === 'idle') { + setIsUploadOpen(undefined) + } + }} + className="relative z-50" + transition + > + +
+ + + + {error && ( +
{error}
+ )} + + + +
+
+
+
+
+ ) +} diff --git a/app/layouts/admin/form-upload.tsx b/app/layouts/admin/form-upload.tsx deleted file mode 100644 index 425e6c7..0000000 --- a/app/layouts/admin/form-upload.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Button, Input } 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 } = useAdminContext() - const fetcher = useFetcher() - const [disabled, setDisabled] = useState(false) - const [error, setError] = useState() - const maxFileSize = 10 * 1024 // 10MB - - 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) - - 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/layouts/admin/navbar.tsx b/app/layouts/admin/navbar.tsx index 740bcb4..6944b92 100644 --- a/app/layouts/admin/navbar.tsx +++ b/app/layouts/admin/navbar.tsx @@ -1,8 +1,9 @@ -import { Button, Popover, PopoverButton, PopoverPanel } from '@headlessui/react' +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' import { Link, useFetcher, useRouteLoaderData } from 'react-router' import { ChevronIcon } from '~/components/icons/chevron' import { ProfileIcon } from '~/components/icons/profile' +import { Button } from '~/components/ui/button' import { APP } from '~/configs/meta' import type { loader } from '~/routes/_admin.lg-admin' @@ -52,8 +53,10 @@ export const Navbar = () => { className="grid" > diff --git a/app/layouts/admin/sidebar.tsx b/app/layouts/admin/sidebar.tsx index 4d7c0e2..f59cef4 100644 --- a/app/layouts/admin/sidebar.tsx +++ b/app/layouts/admin/sidebar.tsx @@ -22,7 +22,7 @@ export const Sidebar = () => { key={`${group}-${title}`} className={twMerge( path === url ? 'bg-[#707FDD]/10 font-bold' : '', - 'group/menu flex h-[42px] w-[200px] items-center gap-x-3 rounded-md px-5 transition-all hover:bg-[#707FDD]/10', + 'group/menu flex h-[42px] w-[200px] items-center gap-x-3 rounded-md px-5 transition hover:bg-[#707FDD]/10 active:bg-[#707FDD]/20', )} > { {/* Tombol Masuk */} - diff --git a/app/layouts/news/form-login.tsx b/app/layouts/news/form-login.tsx index fc7d128..131d249 100644 --- a/app/layouts/news/form-login.tsx +++ b/app/layouts/news/form-login.tsx @@ -24,7 +24,6 @@ export const FormLogin = () => { } = useNewsContext() const fetcher = useFetcher() const [error, setError] = useState() - const [disabled, setDisabled] = useState(false) const formMethods = useRemixForm({ mode: 'onSubmit', @@ -37,11 +36,9 @@ export const FormLogin = () => { useEffect(() => { if (!fetcher.data?.success) { setError(fetcher.data?.message) - setDisabled(false) return } - setDisabled(true) setError(undefined) setIsLoginOpen(false) @@ -87,7 +84,6 @@ export const FormLogin = () => { setIsLoginOpen(false) setIsForgetOpen(true) }} - className="font-semibold text-[#2E2F7C]" variant="link" size="fit" > @@ -96,9 +92,10 @@ export const FormLogin = () => { @@ -113,7 +110,6 @@ export const FormLogin = () => { setIsLoginOpen(false) setIsRegisterOpen(true) }} - className="font-semibold text-[#2E2F7C]" variant="link" size="fit" > diff --git a/app/layouts/news/form-register.tsx b/app/layouts/news/form-register.tsx index cc64940..ad22dab 100644 --- a/app/layouts/news/form-register.tsx +++ b/app/layouts/news/form-register.tsx @@ -40,7 +40,6 @@ export const FormRegister = () => { const { setIsLoginOpen, setIsRegisterOpen, setIsSuccessOpen } = useNewsContext() const [error, setError] = useState() - const [disabled, setDisabled] = useState(false) const fetcher = useFetcher() const loaderData = useRouteLoaderData('routes/_news') const { subscriptionsData: subscriptions } = loaderData || {} @@ -56,11 +55,9 @@ export const FormRegister = () => { useEffect(() => { if (!fetcher.data?.success) { setError(fetcher.data?.message) - setDisabled(false) return } - setDisabled(true) setError(undefined) setIsRegisterOpen(false) setIsSuccessOpen('register') @@ -120,9 +117,10 @@ export const FormRegister = () => { )} @@ -137,7 +135,6 @@ export const FormRegister = () => { setIsLoginOpen(true) setIsRegisterOpen(false) }} - className="font-semibold text-[#2E2F7C]" variant="link" size="fit" > diff --git a/app/layouts/news/form-subscription.tsx b/app/layouts/news/form-subscription.tsx index 6491dff..6905100 100644 --- a/app/layouts/news/form-subscription.tsx +++ b/app/layouts/news/form-subscription.tsx @@ -29,7 +29,6 @@ export default function FormSubscription() { const { setIsSubscribeOpen, setIsSuccessOpen } = useNewsContext() const fetcher = useFetcher() const [error, setError] = useState() - const [disabled, setDisabled] = useState(false) const loaderData = useRouteLoaderData('routes/_news') const { subscriptionsData: subscriptions } = loaderData || {} @@ -44,11 +43,9 @@ export default function FormSubscription() { useEffect(() => { if (!fetcher.data?.success) { setError(fetcher.data?.message) - setDisabled(false) return } - setDisabled(true) setError(undefined) setIsSubscribeOpen(false) setIsSuccessOpen('payment') @@ -77,9 +74,10 @@ export default function FormSubscription() { )} diff --git a/app/layouts/news/header-menu-mobile.tsx b/app/layouts/news/header-menu-mobile.tsx index 78829a7..edbde65 100644 --- a/app/layouts/news/header-menu-mobile.tsx +++ b/app/layouts/news/header-menu-mobile.tsx @@ -71,7 +71,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) { > ))} diff --git a/app/layouts/news/header-top.tsx b/app/layouts/news/header-top.tsx index ba87ae7..434a815 100644 --- a/app/layouts/news/header-top.tsx +++ b/app/layouts/news/header-top.tsx @@ -34,8 +34,10 @@ export const HeaderTop = () => { > diff --git a/app/pages/dashboard-contents/index.tsx b/app/pages/dashboard-contents/index.tsx index 487ac0c..3a01704 100644 --- a/app/pages/dashboard-contents/index.tsx +++ b/app/pages/dashboard-contents/index.tsx @@ -60,7 +60,9 @@ export const ContentsPage = () => { 2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
{data.author.name}
-
ID: {data.id.slice(0, 8)}
+
+ ID: {data.author.id.slice(0, 8)} +
), 3: (value: string) => {value}, diff --git a/app/pages/form-category/index.tsx b/app/pages/form-category/index.tsx index 1dce834..b5a60e7 100644 --- a/app/pages/form-category/index.tsx +++ b/app/pages/form-category/index.tsx @@ -39,7 +39,6 @@ export const FormCategoryPage = (properties: TProperties) => { }, }) const [error, setError] = useState() - const [disabled, setDisabled] = useState(false) const { handleSubmit, watch, setValue } = formMethods const watchName = watch('name') @@ -47,11 +46,9 @@ export const FormCategoryPage = (properties: TProperties) => { useEffect(() => { if (!fetcher.data?.success) { setError(fetcher.data?.message) - setDisabled(false) return } navigate('/lg-admin/categories') - setDisabled(true) setError(undefined) // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetcher]) @@ -96,7 +93,8 @@ export const FormCategoryPage = (properties: TProperties) => { containerClassName="flex-1" /> diff --git a/app/routes/actions.admin.upload.tsx b/app/routes/actions.admin.upload.tsx index 6faa5d9..e31310f 100644 --- a/app/routes/actions.admin.upload.tsx +++ b/app/routes/actions.admin.upload.tsx @@ -4,7 +4,7 @@ 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 { uploadSchema, type TUploadSchema } from '~/layouts/admin/dialog-upload' import { handleCookie } from '~/libs/cookies' import type { Route } from './+types/actions.register'