Merge remote-tracking branch 'origin/master' into feature/slicing

This commit is contained in:
fredy.siswanto 2025-03-11 17:10:33 +07:00
commit ea6462f3ea
24 changed files with 243 additions and 239 deletions

View File

@ -25,6 +25,8 @@
"labelClassName",
"buttonClassName",
"leftNodeClassName",
"rightNodeClassName"
"rightNodeClassName",
"buttonVariants",
"cva"
]
}

View File

@ -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({

View File

@ -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 (
<button
<Button
type="button"
onClick={onClick}
disabled={disabled}
className={twMerge(
'flex h-6 w-8 cursor-pointer items-center justify-center rounded-md p-2 hover:bg-[#2E2F7C] hover:text-white disabled:cursor-not-allowed disabled:bg-[#2E2F7C]/50 disabled:text-white disabled:opacity-50',
'flex h-6 w-8 cursor-pointer items-center justify-center rounded-md p-2 text-black hover:bg-[#2E2F7C] hover:text-white active:bg-[#2E2F7C]/50 disabled:cursor-not-allowed disabled:bg-[#2E2F7C]/50 disabled:text-white disabled:opacity-50',
isActive ? 'bg-[#2E2F7C]/10' : '',
className,
)}
@ -29,6 +30,6 @@ export const EditorButton = (properties: TProperties) => {
title={title}
>
{children}
</button>
</Button>
)
}

View File

@ -127,7 +127,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive('bold')}
title="Bold"
>
<BoldIcon />
<BoldIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().toggleItalic().run()}
@ -137,7 +137,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive('italic')}
title="Italic"
>
<ItalicIcon />
<ItalicIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().toggleStrike().run()}
@ -147,7 +147,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive('strike')}
title="Strike"
>
<StrikethroughIcon />
<StrikethroughIcon className="size-4" />
</EditorButton>
<div className="relative">
<EditorButton
@ -159,7 +159,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={true}
disabled={disabled}
>
<SwatchIcon />
<SwatchIcon className="size-4" />
</EditorButton>
{isOpenColor && (
<div
@ -191,7 +191,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive({ textAlign: 'left' })}
title="Align Left"
>
<Bars3BottomLeftIcon />
<Bars3BottomLeftIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().setTextAlign('center').run()}
@ -202,7 +202,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive({ textAlign: 'center' })}
title="Align Center"
>
<Bars3Icon />
<Bars3Icon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().setTextAlign('right').run()}
@ -213,7 +213,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive({ textAlign: 'right' })}
title="Align Right"
>
<Bars3BottomRightIcon />
<Bars3BottomRightIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
@ -224,7 +224,7 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive({ textAlign: 'justify' })}
title="Align Justify"
>
<Bars4Icon />
<Bars4Icon className="size-4" />
</EditorButton>
</div>
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
@ -236,7 +236,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Heading 1"
disabled={disabled}
>
<H1Icon />
<H1Icon className="size-4" />
</EditorButton>
<EditorButton
onClick={() =>
@ -246,7 +246,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Heading 2"
disabled={disabled}
>
<H2Icon />
<H2Icon className="size-4" />
</EditorButton>
<EditorButton
onClick={() =>
@ -256,7 +256,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Heading 3"
disabled={disabled}
>
<H3Icon />
<H3Icon className="size-4" />
</EditorButton>
{/* <EditorButton
onClick={() => editor.chain().focus().setParagraph().run()}
@ -272,7 +272,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Bullet List"
disabled={disabled}
>
<ListBulletIcon />
<ListBulletIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
@ -280,7 +280,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Ordered List"
disabled={disabled}
>
<NumberedListIcon />
<NumberedListIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
@ -288,7 +288,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Code Block"
disabled={disabled}
>
<CodeBracketIcon />
<CodeBracketIcon className="size-4" />
</EditorButton>
</div>
{/* <div className="flex items-start gap-1 px-1">
@ -334,7 +334,7 @@ export const EditorMenuBar = (properties: TProperties) => {
title="Insert Image"
disabled={disabled}
>
<PhotoIcon />
<PhotoIcon className="size-4" />
</EditorButton>
{isOpenImage && (
<div
@ -380,14 +380,14 @@ export const EditorMenuBar = (properties: TProperties) => {
isActive={editor.isActive('link')}
title="Set Link"
>
<LinkIcon />
<LinkIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().unsetLink().run()}
disabled={disabled || !editor.isActive('link')}
title="Unset Link"
>
<LinkSlashIcon />
<LinkSlashIcon className="size-4" />
</EditorButton>
</div>
<div className="flex items-start gap-1 px-1">
@ -396,14 +396,14 @@ export const EditorMenuBar = (properties: TProperties) => {
disabled={disabled || !editor.can().chain().focus().undo().run()}
title="Undo"
>
<ArrowUturnLeftIcon />
<ArrowUturnLeftIcon className="size-4" />
</EditorButton>
<EditorButton
onClick={() => editor.chain().focus().redo().run()}
disabled={disabled || !editor.can().chain().focus().redo().run()}
title="Redo"
>
<ArrowUturnRightIcon />
<ArrowUturnRightIcon className="size-4" />
</EditorButton>
</div>
</div>
@ -413,7 +413,7 @@ export const EditorMenuBar = (properties: TProperties) => {
onClick={() => setIsPlainHTML(true)}
title="Switch to Plain Text"
>
<DocumentTextIcon />
<DocumentTextIcon className="size-4" />
</EditorButton>
</div>
</div>

View File

@ -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<typeof buttonVariants>['variant']
size?: VariantProps<typeof buttonVariants>['size']
className?: string
isLoading?: boolean
}
type PolymorphicReference<C extends ElementType> =
@ -45,22 +50,27 @@ type ButtonProperties<C extends ElementType> = ButtonBaseProperties & {
ref?: PolymorphicReference<C>
} & Omit<ComponentPropsWithoutRef<C>, keyof ButtonBaseProperties>
export const Button = <C extends ElementType = 'button'>({
export const Button = <C extends ElementType = 'button'>(
properties: ButtonProperties<C>,
) => {
const {
as,
children,
variant,
size,
className,
...properties
}: ButtonProperties<C>) => {
isLoading = false,
...restProperties
} = properties
const Component = as || HeadlessButton
const classes = twMerge(buttonVariants({ variant, size, className }))
return (
<Component
className={classes}
{...properties}
{...restProperties}
>
{isLoading && <ArrowPathIcon className="animate-spin" />}
{children}
</Component>
)

View File

@ -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 (
<div className="flex flex-col">
<Navbar />
@ -18,27 +14,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
</div>
<Dialog
open={!!isUploadOpen}
onClose={() => {
setIsUploadOpen(undefined)
}}
className="relative z-50"
transition
>
<DialogBackdrop
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
transition
/>
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
<DialogPanel
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"
>
<FormUpload />
</DialogPanel>
</div>
</Dialog>
<DialogUpload />
</div>
)
}

View File

@ -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<typeof uploadSchema>
export const DialogUpload = () => {
const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const maxFileSize = 10 * 1024 // 10MB
const formMethods = useRemixForm<TUploadSchema>({
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<HTMLInputElement>) {
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<HTMLInputElement>,
) {
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<HTMLInputElement>) => {
const files = event.target.files
if (files && files.length > 0) {
const file = files[0]
setValue('file', file)
}
}
return (
<Dialog
open={!!isUploadOpen}
onClose={() => {
if (fetcher.state === 'idle') {
setIsUploadOpen(undefined)
}
}}
className="relative z-50"
transition
>
<DialogBackdrop
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
transition
/>
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
<DialogPanel
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"
>
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/admin/upload"
encType="multipart/form-data"
>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Input
type="file"
id="input-file-upload"
accept="image/*"
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
onChange={handleChange}
/>
<input
type="hidden"
id="input-file-upload-type"
value={isUploadOpen}
{...register('category')}
/>
<Button
disabled={fetcher.state !== 'idle'}
isLoading={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md py-2"
>
Upload
</Button>
</fetcher.Form>
</RemixFormProvider>
</DialogPanel>
</div>
</Dialog>
)
}

View File

@ -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<typeof uploadSchema>
export const FormUpload = () => {
const { isUploadOpen, setUploadedFile } = useAdminContext()
const fetcher = useFetcher()
const [disabled, setDisabled] = useState(false)
const [error, setError] = useState<string>()
const maxFileSize = 10 * 1024 // 10MB
const formMethods = useRemixForm<TUploadSchema>({
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<HTMLInputElement>) {
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<HTMLInputElement>,
) {
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<HTMLInputElement>) => {
const files = event.target.files
if (files && files.length > 0) {
const file = files[0]
setValue('file', file)
}
}
return (
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/admin/upload"
encType="multipart/form-data"
>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Input
type="file"
id="input-file-upload"
accept="image/*"
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
onChange={handleChange}
/>
<input
type="hidden"
id="input-file-upload-type"
value={isUploadOpen}
{...register('category')}
/>
<Button
disabled={disabled}
type="submit"
className="w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800"
>
Upload
</Button>
</fetcher.Form>
</RemixFormProvider>
)
}

View File

@ -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"
>
<Button
disabled={fetcher.state !== 'idle'}
isLoading={fetcher.state !== 'idle'}
type="submit"
className="cursor-pointer rounded p-1 hover:bg-[#707FDD]/10 hover:text-[#5363AB]"
className="w-full rounded p-1"
>
Logout
</Button>

View File

@ -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',
)}
>
<Icon

View File

@ -22,7 +22,7 @@ export const FormForgotPassword = () => {
</div>
{/* Tombol Masuk */}
<Button className="mt-5 w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800">
<Button className="mt-5 w-full rounded-md py-2">
Reset Password
</Button>
</form>

View File

@ -24,7 +24,6 @@ export const FormLogin = () => {
} = useNewsContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const formMethods = useRemixForm<TLoginSchema>({
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 = () => {
</div>
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800"
className="w-full rounded-md py-2"
>
Masuk
</Button>
@ -113,7 +110,6 @@ export const FormLogin = () => {
setIsLoginOpen(false)
setIsRegisterOpen(true)
}}
className="font-semibold text-[#2E2F7C]"
variant="link"
size="fit"
>

View File

@ -40,7 +40,6 @@ export const FormRegister = () => {
const { setIsLoginOpen, setIsRegisterOpen, setIsSuccessOpen } =
useNewsContext()
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const fetcher = useFetcher()
const loaderData = useRouteLoaderData<typeof loader>('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 = () => {
)}
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800"
className="w-full rounded-md py-2"
>
Daftar
</Button>
@ -137,7 +135,6 @@ export const FormRegister = () => {
setIsLoginOpen(true)
setIsRegisterOpen(false)
}}
className="font-semibold text-[#2E2F7C]"
variant="link"
size="fit"
>

View File

@ -29,7 +29,6 @@ export default function FormSubscription() {
const { setIsSubscribeOpen, setIsSuccessOpen } = useNewsContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const loaderData = useRouteLoaderData<typeof loader>('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() {
)}
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="mt-5 w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800"
className="mt-5 w-full rounded-md py-2"
>
Lanjutkan
</Button>

View File

@ -71,7 +71,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
>
<Button
variant="newsSecondary"
className="w-full bg-white px-[35px] py-3 text-center text-[#2E2F7C] sm:hidden"
className="w-full px-[35px] py-3 text-center sm:hidden"
type="submit"
>
Logout
@ -80,7 +80,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
) : (
<Button
variant="newsSecondary"
className="w-full bg-white px-[35px] py-3 text-center text-[#2E2F7C] sm:hidden"
className="w-full px-[35px] py-3 text-center sm:hidden"
onClick={() => {
setIsMenuOpen(false)
setIsLoginOpen(true)

View File

@ -1,5 +1,6 @@
import { Link, useRouteLoaderData } from 'react-router'
import { Button } from '~/components/ui/button'
import HeaderMenuMobile from '~/layouts/news/header-menu-mobile'
import type { loader } from '~/routes/_news'
@ -17,15 +18,17 @@ export const HeaderMenu = () => {
<>
<div className="hidden h-[60px] items-center justify-between bg-[#2E2F7C] text-xl font-medium text-white sm:flex">
{menu?.map((item) => (
<Link
<Button
as={Link}
key={item.id}
to={`/category/${item.code}`}
size="fit"
className={
'flex h-full items-center justify-center border-r border-white px-[35px]'
'flex h-full items-center justify-center border-r border-white px-[35px] text-xl'
}
>
{item.name}
</Link>
</Button>
))}
<HeaderSearch />
</div>

View File

@ -34,8 +34,10 @@ export const HeaderTop = () => {
>
<Button
variant="newsSecondary"
className="hidden sm:block"
className="hidden sm:flex"
type="submit"
disabled={fetcher.state !== 'idle'}
isLoading={fetcher.state !== 'idle'}
>
Logout
</Button>

View File

@ -60,7 +60,9 @@ export const ContentsPage = () => {
2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
<div>
<div>{data.author.name}</div>
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
<div className="text-sm text-[#7C7C7C]">
ID: {data.author.id.slice(0, 8)}
</div>
</div>
),
3: (value: string) => <span className="text-sm">{value}</span>,

View File

@ -39,7 +39,6 @@ export const FormCategoryPage = (properties: TProperties) => {
},
})
const [error, setError] = useState<string>()
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"
/>
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
size="lg"
className="text-md h-[42px] rounded-md"

View File

@ -73,7 +73,6 @@ export const FormContentsPage = (properties: TProperties) => {
const { categoriesData: categories } = loaderData || {}
const { tagsData: tags } = loaderData || {}
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const formMethods = useRemixForm<TContentSchema>({
mode: 'onSubmit',
@ -100,12 +99,10 @@ export const FormContentsPage = (properties: TProperties) => {
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
setDisabled(false)
return
}
navigate('/lg-admin/contents')
setDisabled(true)
setError(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
@ -144,7 +141,8 @@ export const FormContentsPage = (properties: TProperties) => {
containerClassName="flex-1"
/>
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
size="lg"
className="text-md h-[42px] rounded-md"

View File

@ -41,7 +41,6 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
},
})
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const { handleSubmit, watch, setValue } = formMethods
const watchName = watch('name')
@ -49,11 +48,9 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
setDisabled(false)
return
}
navigate('/lg-admin/subscribe-plan')
setDisabled(true)
setError(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
@ -100,7 +97,8 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
containerClassName="flex-1"
/>
<Button
disabled={disabled}
disabled={fetcher.state !== 'idle'}
isLoading={fetcher.state !== 'idle'}
type="submit"
size="lg"
className="text-md h-[42px] rounded-md"

View File

@ -35,7 +35,6 @@ export const FormTagPage = (properties: TProperties) => {
},
})
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const { handleSubmit, watch, setValue } = formMethods
const watchName = watch('name')
@ -43,11 +42,9 @@ export const FormTagPage = (properties: TProperties) => {
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
setDisabled(false)
return
}
navigate('/lg-admin/tags')
setDisabled(true)
setError(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
@ -92,7 +89,8 @@ export const FormTagPage = (properties: TProperties) => {
containerClassName="flex-1"
/>
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
size="lg"
className="text-md h-[42px] rounded-md"

View File

@ -23,14 +23,12 @@ export const AdminLoginPage = () => {
resolver: zodResolver(loginSchema),
})
const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false)
const { handleSubmit } = formMethods
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
setDisabled(false)
return
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -81,18 +79,21 @@ export const AdminLoginPage = () => {
{/* Lupa Kata Sandi */}
<div className="mb-4 flex justify-between">
<span className="text-gray-600">Lupa Kata Sandi?</span>
<Link
<Button
as={Link}
variant={'link'}
size="fit"
to="/lg-admin/auth/reset-password"
className="font-semibold text-[#2E2F7C]"
>
Reset Kata Sandi
</Link>
</Button>
</div>
<Button
disabled={disabled}
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800"
className="w-full rounded-md py-2"
>
Masuk
</Button>

View File

@ -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'