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}
-
+
)
}
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 (
+
+ )
+}
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 */}
-