Merge remote-tracking branch 'origin/master' into feature/slicing
This commit is contained in:
commit
ea6462f3ea
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -25,6 +25,8 @@
|
|||||||
"labelClassName",
|
"labelClassName",
|
||||||
"buttonClassName",
|
"buttonClassName",
|
||||||
"leftNodeClassName",
|
"leftNodeClassName",
|
||||||
"rightNodeClassName"
|
"rightNodeClassName",
|
||||||
|
"buttonVariants",
|
||||||
|
"cva"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod'
|
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'
|
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||||
|
|
||||||
const uploadResponseSchema = z.object({
|
const uploadResponseSchema = z.object({
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from '@headlessui/react'
|
||||||
import type { CSSProperties, MouseEventHandler, ReactNode } from 'react'
|
import type { CSSProperties, MouseEventHandler, ReactNode } from 'react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@ -16,12 +17,12 @@ export const EditorButton = (properties: TProperties) => {
|
|||||||
properties
|
properties
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={twMerge(
|
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' : '',
|
isActive ? 'bg-[#2E2F7C]/10' : '',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@ -29,6 +30,6 @@ export const EditorButton = (properties: TProperties) => {
|
|||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive('bold')}
|
isActive={editor.isActive('bold')}
|
||||||
title="Bold"
|
title="Bold"
|
||||||
>
|
>
|
||||||
<BoldIcon />
|
<BoldIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
@ -137,7 +137,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive('italic')}
|
isActive={editor.isActive('italic')}
|
||||||
title="Italic"
|
title="Italic"
|
||||||
>
|
>
|
||||||
<ItalicIcon />
|
<ItalicIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
@ -147,7 +147,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive('strike')}
|
isActive={editor.isActive('strike')}
|
||||||
title="Strike"
|
title="Strike"
|
||||||
>
|
>
|
||||||
<StrikethroughIcon />
|
<StrikethroughIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<EditorButton
|
<EditorButton
|
||||||
@ -159,7 +159,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={true}
|
isActive={true}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SwatchIcon />
|
<SwatchIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
{isOpenColor && (
|
{isOpenColor && (
|
||||||
<div
|
<div
|
||||||
@ -191,7 +191,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive({ textAlign: 'left' })}
|
isActive={editor.isActive({ textAlign: 'left' })}
|
||||||
title="Align Left"
|
title="Align Left"
|
||||||
>
|
>
|
||||||
<Bars3BottomLeftIcon />
|
<Bars3BottomLeftIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||||
@ -202,7 +202,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive({ textAlign: 'center' })}
|
isActive={editor.isActive({ textAlign: 'center' })}
|
||||||
title="Align Center"
|
title="Align Center"
|
||||||
>
|
>
|
||||||
<Bars3Icon />
|
<Bars3Icon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
@ -213,7 +213,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive({ textAlign: 'right' })}
|
isActive={editor.isActive({ textAlign: 'right' })}
|
||||||
title="Align Right"
|
title="Align Right"
|
||||||
>
|
>
|
||||||
<Bars3BottomRightIcon />
|
<Bars3BottomRightIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||||
@ -224,7 +224,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive({ textAlign: 'justify' })}
|
isActive={editor.isActive({ textAlign: 'justify' })}
|
||||||
title="Align Justify"
|
title="Align Justify"
|
||||||
>
|
>
|
||||||
<Bars4Icon />
|
<Bars4Icon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
|
<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"
|
title="Heading 1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<H1Icon />
|
<H1Icon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -246,7 +246,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Heading 2"
|
title="Heading 2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<H2Icon />
|
<H2Icon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -256,7 +256,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Heading 3"
|
title="Heading 3"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<H3Icon />
|
<H3Icon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
{/* <EditorButton
|
{/* <EditorButton
|
||||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||||
@ -272,7 +272,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Bullet List"
|
title="Bullet List"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<ListBulletIcon />
|
<ListBulletIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
@ -280,7 +280,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Ordered List"
|
title="Ordered List"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<NumberedListIcon />
|
<NumberedListIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
@ -288,7 +288,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Code Block"
|
title="Code Block"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<CodeBracketIcon />
|
<CodeBracketIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex items-start gap-1 px-1">
|
{/* <div className="flex items-start gap-1 px-1">
|
||||||
@ -334,7 +334,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
title="Insert Image"
|
title="Insert Image"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<PhotoIcon />
|
<PhotoIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
{isOpenImage && (
|
{isOpenImage && (
|
||||||
<div
|
<div
|
||||||
@ -380,14 +380,14 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
isActive={editor.isActive('link')}
|
isActive={editor.isActive('link')}
|
||||||
title="Set Link"
|
title="Set Link"
|
||||||
>
|
>
|
||||||
<LinkIcon />
|
<LinkIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||||
disabled={disabled || !editor.isActive('link')}
|
disabled={disabled || !editor.isActive('link')}
|
||||||
title="Unset Link"
|
title="Unset Link"
|
||||||
>
|
>
|
||||||
<LinkSlashIcon />
|
<LinkSlashIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-1 px-1">
|
<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()}
|
disabled={disabled || !editor.can().chain().focus().undo().run()}
|
||||||
title="Undo"
|
title="Undo"
|
||||||
>
|
>
|
||||||
<ArrowUturnLeftIcon />
|
<ArrowUturnLeftIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
<EditorButton
|
<EditorButton
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
disabled={disabled || !editor.can().chain().focus().redo().run()}
|
disabled={disabled || !editor.can().chain().focus().redo().run()}
|
||||||
title="Redo"
|
title="Redo"
|
||||||
>
|
>
|
||||||
<ArrowUturnRightIcon />
|
<ArrowUturnRightIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -413,7 +413,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
|||||||
onClick={() => setIsPlainHTML(true)}
|
onClick={() => setIsPlainHTML(true)}
|
||||||
title="Switch to Plain Text"
|
title="Switch to Plain Text"
|
||||||
>
|
>
|
||||||
<DocumentTextIcon />
|
<DocumentTextIcon className="size-4" />
|
||||||
</EditorButton>
|
</EditorButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Button as HeadlessButton } from '@headlessui/react'
|
import { Button as HeadlessButton } from '@headlessui/react'
|
||||||
|
import { ArrowPathIcon } from '@heroicons/react/20/solid'
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'
|
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
@ -8,11 +9,14 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
newsPrimary: 'bg-[#2E2F7C] text-white text-lg',
|
newsPrimary:
|
||||||
newsPrimaryOutline: 'border-[3px] border-white text-white text-lg',
|
'bg-[#2E2F7C] text-white text-lg hover:bg-[#4C5CA0] hover:shadow transition active:bg-[#6970B4]',
|
||||||
newsSecondary: 'border-[3px] border-[#2E2F7C] text-[#2E2F7C] text-lg',
|
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: '',
|
icon: '',
|
||||||
link: '',
|
link: 'font-semibold text-[#2E2F7C] hover:text-[#4C5CA0] active:text-[#6970B4] transition',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-[50px] w-[150px]',
|
default: 'h-[50px] w-[150px]',
|
||||||
@ -35,6 +39,7 @@ type ButtonBaseProperties = {
|
|||||||
variant?: VariantProps<typeof buttonVariants>['variant']
|
variant?: VariantProps<typeof buttonVariants>['variant']
|
||||||
size?: VariantProps<typeof buttonVariants>['size']
|
size?: VariantProps<typeof buttonVariants>['size']
|
||||||
className?: string
|
className?: string
|
||||||
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolymorphicReference<C extends ElementType> =
|
type PolymorphicReference<C extends ElementType> =
|
||||||
@ -45,22 +50,27 @@ type ButtonProperties<C extends ElementType> = ButtonBaseProperties & {
|
|||||||
ref?: PolymorphicReference<C>
|
ref?: PolymorphicReference<C>
|
||||||
} & Omit<ComponentPropsWithoutRef<C>, keyof ButtonBaseProperties>
|
} & Omit<ComponentPropsWithoutRef<C>, keyof ButtonBaseProperties>
|
||||||
|
|
||||||
export const Button = <C extends ElementType = 'button'>({
|
export const Button = <C extends ElementType = 'button'>(
|
||||||
|
properties: ButtonProperties<C>,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
as,
|
as,
|
||||||
children,
|
children,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
className,
|
className,
|
||||||
...properties
|
isLoading = false,
|
||||||
}: ButtonProperties<C>) => {
|
...restProperties
|
||||||
|
} = properties
|
||||||
const Component = as || HeadlessButton
|
const Component = as || HeadlessButton
|
||||||
const classes = twMerge(buttonVariants({ variant, size, className }))
|
const classes = twMerge(buttonVariants({ variant, size, className }))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={classes}
|
className={classes}
|
||||||
{...properties}
|
{...restProperties}
|
||||||
>
|
>
|
||||||
|
{isLoading && <ArrowPathIcon className="animate-spin" />}
|
||||||
{children}
|
{children}
|
||||||
</Component>
|
</Component>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
import { useAdminContext } from '~/contexts/admin'
|
import { DialogUpload } from './dialog-upload'
|
||||||
|
|
||||||
import { FormUpload } from './form-upload'
|
|
||||||
import { Navbar } from './navbar'
|
import { Navbar } from './navbar'
|
||||||
import { Sidebar } from './sidebar'
|
import { Sidebar } from './sidebar'
|
||||||
|
|
||||||
export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
||||||
const { children } = properties
|
const { children } = properties
|
||||||
const { isUploadOpen, setIsUploadOpen } = useAdminContext()
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -18,27 +14,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
|||||||
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
|
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<DialogUpload />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
143
app/layouts/admin/dialog-upload.tsx
Normal file
143
app/layouts/admin/dialog-upload.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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 { Link, useFetcher, useRouteLoaderData } from 'react-router'
|
||||||
|
|
||||||
import { ChevronIcon } from '~/components/icons/chevron'
|
import { ChevronIcon } from '~/components/icons/chevron'
|
||||||
import { ProfileIcon } from '~/components/icons/profile'
|
import { ProfileIcon } from '~/components/icons/profile'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
import { APP } from '~/configs/meta'
|
import { APP } from '~/configs/meta'
|
||||||
import type { loader } from '~/routes/_admin.lg-admin'
|
import type { loader } from '~/routes/_admin.lg-admin'
|
||||||
|
|
||||||
@ -52,8 +53,10 @@ export const Navbar = () => {
|
|||||||
className="grid"
|
className="grid"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
|
isLoading={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="cursor-pointer rounded p-1 hover:bg-[#707FDD]/10 hover:text-[#5363AB]"
|
className="w-full rounded p-1"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const Sidebar = () => {
|
|||||||
key={`${group}-${title}`}
|
key={`${group}-${title}`}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
path === url ? 'bg-[#707FDD]/10 font-bold' : '',
|
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
|
<Icon
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const FormForgotPassword = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tombol Masuk */}
|
{/* 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
|
Reset Password
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -24,7 +24,6 @@ export const FormLogin = () => {
|
|||||||
} = useNewsContext()
|
} = useNewsContext()
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const formMethods = useRemixForm<TLoginSchema>({
|
const formMethods = useRemixForm<TLoginSchema>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@ -37,11 +36,9 @@ export const FormLogin = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsLoginOpen(false)
|
setIsLoginOpen(false)
|
||||||
|
|
||||||
@ -87,7 +84,6 @@ export const FormLogin = () => {
|
|||||||
setIsLoginOpen(false)
|
setIsLoginOpen(false)
|
||||||
setIsForgetOpen(true)
|
setIsForgetOpen(true)
|
||||||
}}
|
}}
|
||||||
className="font-semibold text-[#2E2F7C]"
|
|
||||||
variant="link"
|
variant="link"
|
||||||
size="fit"
|
size="fit"
|
||||||
>
|
>
|
||||||
@ -96,9 +92,10 @@ export const FormLogin = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
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
|
Masuk
|
||||||
</Button>
|
</Button>
|
||||||
@ -113,7 +110,6 @@ export const FormLogin = () => {
|
|||||||
setIsLoginOpen(false)
|
setIsLoginOpen(false)
|
||||||
setIsRegisterOpen(true)
|
setIsRegisterOpen(true)
|
||||||
}}
|
}}
|
||||||
className="font-semibold text-[#2E2F7C]"
|
|
||||||
variant="link"
|
variant="link"
|
||||||
size="fit"
|
size="fit"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -40,7 +40,6 @@ export const FormRegister = () => {
|
|||||||
const { setIsLoginOpen, setIsRegisterOpen, setIsSuccessOpen } =
|
const { setIsLoginOpen, setIsRegisterOpen, setIsSuccessOpen } =
|
||||||
useNewsContext()
|
useNewsContext()
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||||
const { subscriptionsData: subscriptions } = loaderData || {}
|
const { subscriptionsData: subscriptions } = loaderData || {}
|
||||||
@ -56,11 +55,9 @@ export const FormRegister = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsRegisterOpen(false)
|
setIsRegisterOpen(false)
|
||||||
setIsSuccessOpen('register')
|
setIsSuccessOpen('register')
|
||||||
@ -120,9 +117,10 @@ export const FormRegister = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
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
|
Daftar
|
||||||
</Button>
|
</Button>
|
||||||
@ -137,7 +135,6 @@ export const FormRegister = () => {
|
|||||||
setIsLoginOpen(true)
|
setIsLoginOpen(true)
|
||||||
setIsRegisterOpen(false)
|
setIsRegisterOpen(false)
|
||||||
}}
|
}}
|
||||||
className="font-semibold text-[#2E2F7C]"
|
|
||||||
variant="link"
|
variant="link"
|
||||||
size="fit"
|
size="fit"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export default function FormSubscription() {
|
|||||||
const { setIsSubscribeOpen, setIsSuccessOpen } = useNewsContext()
|
const { setIsSubscribeOpen, setIsSuccessOpen } = useNewsContext()
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||||
const { subscriptionsData: subscriptions } = loaderData || {}
|
const { subscriptionsData: subscriptions } = loaderData || {}
|
||||||
|
|
||||||
@ -44,11 +43,9 @@ export default function FormSubscription() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubscribeOpen(false)
|
setIsSubscribeOpen(false)
|
||||||
setIsSuccessOpen('payment')
|
setIsSuccessOpen('payment')
|
||||||
@ -77,9 +74,10 @@ export default function FormSubscription() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
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
|
Lanjutkan
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="newsSecondary"
|
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"
|
type="submit"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
@ -80,7 +80,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="newsSecondary"
|
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={() => {
|
onClick={() => {
|
||||||
setIsMenuOpen(false)
|
setIsMenuOpen(false)
|
||||||
setIsLoginOpen(true)
|
setIsLoginOpen(true)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Link, useRouteLoaderData } from 'react-router'
|
import { Link, useRouteLoaderData } from 'react-router'
|
||||||
|
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
import HeaderMenuMobile from '~/layouts/news/header-menu-mobile'
|
import HeaderMenuMobile from '~/layouts/news/header-menu-mobile'
|
||||||
import type { loader } from '~/routes/_news'
|
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">
|
<div className="hidden h-[60px] items-center justify-between bg-[#2E2F7C] text-xl font-medium text-white sm:flex">
|
||||||
{menu?.map((item) => (
|
{menu?.map((item) => (
|
||||||
<Link
|
<Button
|
||||||
|
as={Link}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
to={`/category/${item.code}`}
|
to={`/category/${item.code}`}
|
||||||
|
size="fit"
|
||||||
className={
|
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}
|
{item.name}
|
||||||
</Link>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<HeaderSearch />
|
<HeaderSearch />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -34,8 +34,10 @@ export const HeaderTop = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="newsSecondary"
|
variant="newsSecondary"
|
||||||
className="hidden sm:block"
|
className="hidden sm:flex"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
|
isLoading={fetcher.state !== 'idle'}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -60,7 +60,9 @@ export const ContentsPage = () => {
|
|||||||
2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
|
2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
|
||||||
<div>
|
<div>
|
||||||
<div>{data.author.name}</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>
|
</div>
|
||||||
),
|
),
|
||||||
3: (value: string) => <span className="text-sm">{value}</span>,
|
3: (value: string) => <span className="text-sm">{value}</span>,
|
||||||
|
|||||||
@ -39,7 +39,6 @@ export const FormCategoryPage = (properties: TProperties) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const { handleSubmit, watch, setValue } = formMethods
|
const { handleSubmit, watch, setValue } = formMethods
|
||||||
const watchName = watch('name')
|
const watchName = watch('name')
|
||||||
@ -47,11 +46,9 @@ export const FormCategoryPage = (properties: TProperties) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate('/lg-admin/categories')
|
navigate('/lg-admin/categories')
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fetcher])
|
}, [fetcher])
|
||||||
@ -96,7 +93,8 @@ export const FormCategoryPage = (properties: TProperties) => {
|
|||||||
containerClassName="flex-1"
|
containerClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-md h-[42px] rounded-md"
|
className="text-md h-[42px] rounded-md"
|
||||||
|
|||||||
@ -73,7 +73,6 @@ export const FormContentsPage = (properties: TProperties) => {
|
|||||||
const { categoriesData: categories } = loaderData || {}
|
const { categoriesData: categories } = loaderData || {}
|
||||||
const { tagsData: tags } = loaderData || {}
|
const { tagsData: tags } = loaderData || {}
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const formMethods = useRemixForm<TContentSchema>({
|
const formMethods = useRemixForm<TContentSchema>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@ -100,12 +99,10 @@ export const FormContentsPage = (properties: TProperties) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/lg-admin/contents')
|
navigate('/lg-admin/contents')
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fetcher])
|
}, [fetcher])
|
||||||
@ -144,7 +141,8 @@ export const FormContentsPage = (properties: TProperties) => {
|
|||||||
containerClassName="flex-1"
|
containerClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-md h-[42px] rounded-md"
|
className="text-md h-[42px] rounded-md"
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const { handleSubmit, watch, setValue } = formMethods
|
const { handleSubmit, watch, setValue } = formMethods
|
||||||
const watchName = watch('name')
|
const watchName = watch('name')
|
||||||
@ -49,11 +48,9 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate('/lg-admin/subscribe-plan')
|
navigate('/lg-admin/subscribe-plan')
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fetcher])
|
}, [fetcher])
|
||||||
@ -100,7 +97,8 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
|
|||||||
containerClassName="flex-1"
|
containerClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={fetcher.state !== 'idle'}
|
||||||
|
isLoading={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-md h-[42px] rounded-md"
|
className="text-md h-[42px] rounded-md"
|
||||||
|
|||||||
@ -35,7 +35,6 @@ export const FormTagPage = (properties: TProperties) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const { handleSubmit, watch, setValue } = formMethods
|
const { handleSubmit, watch, setValue } = formMethods
|
||||||
const watchName = watch('name')
|
const watchName = watch('name')
|
||||||
@ -43,11 +42,9 @@ export const FormTagPage = (properties: TProperties) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate('/lg-admin/tags')
|
navigate('/lg-admin/tags')
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fetcher])
|
}, [fetcher])
|
||||||
@ -92,7 +89,8 @@ export const FormTagPage = (properties: TProperties) => {
|
|||||||
containerClassName="flex-1"
|
containerClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-md h-[42px] rounded-md"
|
className="text-md h-[42px] rounded-md"
|
||||||
|
|||||||
@ -23,14 +23,12 @@ export const AdminLoginPage = () => {
|
|||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const { handleSubmit } = formMethods
|
const { handleSubmit } = formMethods
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fetcher.data?.success) {
|
if (!fetcher.data?.success) {
|
||||||
setError(fetcher.data?.message)
|
setError(fetcher.data?.message)
|
||||||
setDisabled(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -81,18 +79,21 @@ export const AdminLoginPage = () => {
|
|||||||
{/* Lupa Kata Sandi */}
|
{/* Lupa Kata Sandi */}
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<span className="text-gray-600">Lupa Kata Sandi?</span>
|
<span className="text-gray-600">Lupa Kata Sandi?</span>
|
||||||
<Link
|
<Button
|
||||||
|
as={Link}
|
||||||
|
variant={'link'}
|
||||||
|
size="fit"
|
||||||
to="/lg-admin/auth/reset-password"
|
to="/lg-admin/auth/reset-password"
|
||||||
className="font-semibold text-[#2E2F7C]"
|
|
||||||
>
|
>
|
||||||
Reset Kata Sandi
|
Reset Kata Sandi
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
type="submit"
|
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
|
Masuk
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { getValidatedFormData } from 'remix-hook-form'
|
|||||||
import { XiorError } from 'xior'
|
import { XiorError } from 'xior'
|
||||||
|
|
||||||
import { uploadFileRequest } from '~/apis/admin/upload-file'
|
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 { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.register'
|
import type { Route } from './+types/actions.register'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user