Compare commits

..

19 Commits

Author SHA1 Message Date
Ardeman
9a0c6c1f0b feat: implement file upload functionality and enhance admin dashboard layout 2025-03-10 12:21:08 +08:00
fredy.siswanto
8c2298ff61 Merge remote-tracking branch 'origin/master' into feature/slicing 2025-03-09 23:25:01 +07:00
fredy.siswanto
061f95d4e5 refactor: enhance UiTable styling and add header alignment for improved readability 2025-03-09 23:22:38 +07:00
fredy.siswanto
de92703fbe refactor: implement user management API and update dashboard users page with new data table 2025-03-09 23:06:07 +07:00
fredy.siswanto
a9a350fdb2 refactor: update subscription data loading and clean up unused imports in dashboard components 2025-03-09 20:23:26 +07:00
Ardeman
4bc185434b fix: adjust padding in InputFile and Input components for better layout 2025-03-09 21:20:29 +08:00
Ardeman
4bb7a23795 feat: implement upload dialog in admin dashboard with context management 2025-03-09 20:52:50 +08:00
Ardeman
f291290f55 refactor: handle 401 errors in staff and user data loading with logout headers 2025-03-09 20:46:10 +08:00
Ardeman
e84c05d0a0 Merge commit '5b83c3f8dd8139c24ae4b6c288ab80ac08b43063' 2025-03-09 20:23:04 +08:00
Ardeman
c7583413bb feat: add AdminContext for managing upload state in admin dashboard 2025-03-09 20:22:48 +08:00
fredy.siswanto
5b83c3f8dd refactor: improve type definitions in UiTable, handle potential undefined data in SubscribePlanPage 2025-03-09 19:12:23 +07:00
Ardeman
0f414f6963 refactor: update disabled button styles in EditorButton component for improved accessibility 2025-03-09 15:46:10 +08:00
fredy.siswanto
2940057cb1 Merge remote-tracking branch 'origin/master' into feature/slicing 2025-03-09 14:42:52 +07:00
fredy.siswanto
9dca781d6d refactor: add Subscribe Plan menu item and implement subscription management pages 2025-03-09 14:40:55 +07:00
Ardeman
f17d64eb9b refactor: rename parameters in CategoriesPage for clarity 2025-03-09 15:34:00 +08:00
Ardeman
64bb369c59 refactor: update table component types and enhance admin menu with category and tag 2025-03-09 15:31:10 +08:00
Ardeman
422c9cbfe2 feat: add InputFile component for file uploads and integrate into FormContentsPage 2025-03-09 15:14:57 +08:00
Ardeman
3d13707359 refactor: update placeholder text for featured image input to clarify URL requirement 2025-03-09 14:55:04 +08:00
Ardeman
9f6c07c3de refactor: rename 'Konten' to 'Artikel' and add submenu for article management 2025-03-09 14:54:18 +08:00
33 changed files with 797 additions and 563 deletions

View File

@ -0,0 +1,39 @@
import { z } from 'zod'
import { HttpServer, type THttpServer } from '~/libs/http-server'
const subscribePlanResponseSchema = z.object({
id: z.string(),
code: z.string(),
name: z.string(),
})
const subscribeResponseSchema = z.object({
id: z.string(),
subscribe_plan_id: z.string(),
start_date: z.string(),
end_date: z.string().nullable(),
status: z.string(),
auto_renew: z.boolean(),
subscribe_plan: subscribePlanResponseSchema,
})
const userResponseSchema = z.object({
id: z.string(),
email: z.string().email(),
phone: z.string(),
subscribe: subscribeResponseSchema,
})
const usersResponseSchema = z.object({
data: z.array(userResponseSchema),
})
export type TUserResponse = z.infer<typeof userResponseSchema>
export const getUsers = async (parameters: THttpServer) => {
try {
const { data } = await HttpServer(parameters).get(`/api/staff/users`)
return usersResponseSchema.parse(data)
} catch (error) {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
return Promise.reject(error)
}
}

View File

@ -0,0 +1,37 @@
import { z } from 'zod'
import type { TUploadSchema } from '~/layouts/admin/form-upload'
import { HttpServer, type THttpServer } from '~/libs/http-server'
const uploadResponseSchema = z.object({
data: z.object({
message: z.string(),
data: z.object({
file_path: z.string(),
file_url: z.string(),
}),
}),
})
type TParameter = {
payload: TUploadSchema & {
file: File
}
} & THttpServer
export const uploadFileRequest = async (parameters: TParameter) => {
const { payload, ...restParameters } = parameters
const formdata = new FormData()
formdata.append('file', payload.file)
try {
const { data } = await HttpServer(restParameters).post(
'/api/file',
formdata,
)
return uploadResponseSchema.parse(data)
} catch (error) {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
return Promise.reject(error)
}
}

View File

@ -1,28 +0,0 @@
import type { JSX, SVGProps } from 'react'
/**
* Note: `ChevronDoubleIcon` default mengarah ke kiri.
* Gunakan class `rotate-xx` untuk mengubah arah ikon.
*/
export const ChevronDoubleIcon = (
properties: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) => {
return (
<svg
width={20}
height={20}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={properties.className}
{...properties}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.512 4.427l-2.984 3.58 2.877 3.575a.667.667 0 01-1.04.836l-3.218-4a.667.667 0 01.007-.844l3.334-4a.667.667 0 011.024.853zm-5.69-.853a.667.667 0 011.023.853l-2.984 3.58 2.877 3.575a.667.667 0 01-1.039.836l-3.218-4a.666.666 0 01.007-.844l3.333-4z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -21,7 +21,7 @@ export const EditorButton = (properties: TProperties) => {
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:hover:bg-[#2E2F7C]/50',
'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',
isActive ? 'bg-[#2E2F7C]/10' : '',
className,
)}

View File

@ -71,7 +71,7 @@ export const UiChartPie = () => {
return (
<div className="h-[300px] w-full items-center justify-center rounded-lg bg-white p-5 text-center">
<h2 className="text-xl font-bold">Top 5 Konten</h2>
<h2 className="text-xl font-bold">Top 5 Artikel</h2>
<Pie
height={225}
width={450}

View File

@ -0,0 +1,93 @@
import { Field, Label, Input as HeadlessInput } from '@headlessui/react'
import { CloudArrowUpIcon } from '@heroicons/react/20/solid'
import { useEffect, type ComponentProps, type ReactNode } from 'react'
import {
get,
type FieldError,
type FieldValues,
type Path,
type RegisterOptions,
} from 'react-hook-form'
import { useRemixFormContext } from 'remix-hook-form'
import { twMerge } from 'tailwind-merge'
import { useAdminContext } from '~/contexts/admin'
import { Button } from './button'
type TInputProperties<T extends FieldValues> = Omit<
ComponentProps<'input'>,
'size'
> & {
id: string
label?: ReactNode
name: Path<T>
rules?: RegisterOptions
containerClassName?: string
labelClassName?: string
}
export const InputFile = <TFormValues extends Record<string, unknown>>(
properties: TInputProperties<TFormValues>,
) => {
const {
id,
label,
name,
rules,
placeholder,
disabled,
className,
containerClassName,
labelClassName,
...restProperties
} = properties
const { setIsUploadOpen, uploadedFile } = useAdminContext()
const {
register,
formState: { errors },
setValue,
} = useRemixFormContext()
const error: FieldError = get(errors, name)
useEffect(() => {
if (uploadedFile) {
setValue('featured_image', uploadedFile)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploadedFile])
return (
<Field
className={twMerge('relative', containerClassName)}
disabled={disabled}
id={id}
>
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
{label} {error && <span className="text-red-500">{error.message}</span>}
</Label>
<HeadlessInput
className={twMerge(
'h-[42px] w-full rounded-md border border-[#DFDFDF] p-2 pr-8',
className,
)}
placeholder={placeholder}
{...register(name, rules)}
{...restProperties}
/>
<Button
type="button"
variant="icon"
size="fit"
className="absolute right-3 h-[42px]"
onClick={() => {
setIsUploadOpen('featured_image')
}}
>
<CloudArrowUpIcon className="h-4 w-4 text-gray-500/50" />
</Button>
</Field>
)
}

View File

@ -63,7 +63,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
<HeadlessInput
type={inputType}
className={twMerge(
'h-[42px] w-full rounded-md border border-[#DFDFDF] p-2',
'h-[42px] w-full rounded-md border border-[#DFDFDF] p-2 pr-8',
className,
)}
placeholder={inputType === 'password' ? '******' : placeholder}
@ -75,7 +75,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
type="button"
variant="icon"
size="fit"
className="absolute right-3 h-[42px] text-gray-500"
className="absolute right-3 h-[42px]"
onClick={() =>
setInputType(inputType === 'password' ? 'text' : 'password')
}

View File

@ -1,66 +0,0 @@
import React from 'react'
import { ChevronIcon } from '~/components/icons/chevron'
import { ChevronDoubleIcon } from '~/components/icons/chevron-double'
type PaginationProperties = {
currentPage: number
totalPages: number
onPageChange: (page: number) => void
}
export const Pagination: React.FC<PaginationProperties> = ({
currentPage = 1,
totalPages,
onPageChange,
}) => {
const renderPageNumbers = () => {
const pages = []
for (let index = 1; index <= totalPages; index++) {
pages.push(
<button
key={index}
onClick={() => onPageChange(index)}
className={`rounded-lg px-3 py-1 ${
currentPage === index ? 'bg-[#2E2F7C] text-white' : 'text-gray-500'
}`}
>
{index}
</button>,
)
}
return pages
}
return (
<div className="flex items-center justify-center space-x-2 text-[#2E2F7C]">
<button
onClick={() => onPageChange(1)}
disabled={currentPage === 1}
>
<ChevronDoubleIcon />
</button>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronIcon className="rotate-90" />
</button>
{renderPageNumbers()}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronIcon className="rotate-270" />
</button>
<button
onClick={() => onPageChange(totalPages)}
disabled={currentPage === totalPages}
>
<ChevronDoubleIcon className="rotate-180" />
</button>
</div>
)
}

View File

@ -1,14 +1,10 @@
import DT, { type Config, type ConfigColumns } from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import DT from 'datatables.net-dt'
import DataTable, { type DataTableProps } from 'datatables.net-react'
import React from 'react'
type UiTableProperties = {
data: any // eslint-disable-line @typescript-eslint/no-explicit-any
columns: ConfigColumns[]
slots?: any // eslint-disable-line @typescript-eslint/no-explicit-any
options?: Config
title: string
}
} & DataTableProps
const renderPaginationIcon = (icon: string) => {
return `<div class="pagination-icon">${icon}</div>`
@ -27,11 +23,14 @@ export const UiTable: React.FC<UiTableProperties> = ({
<h3 className="py-1 font-semibold text-[#4C5CA0]">{title}</h3>
<div className="rounded-lg">
<DataTable
className="cell-border"
className="cell-border text-sm"
data={data}
columns={columns}
slots={slots}
options={{
headerCallback: (thead) => {
thead.classList.add('text-left')
},
paging: true,
searching: true,
ordering: true,

View File

@ -1,4 +1,4 @@
import { MENU as ADMIN_MENU } from '~/layouts/admin/menu'
import { MENU as ADMIN_MENU, SUB_MENU } from '~/layouts/admin/menu'
export const APP = {
title: 'LegalGo',
@ -23,4 +23,5 @@ export const META_TITLE_CONFIG: TMetaTitleConfig = [
...ADMIN_MENU.flatMap((menu) =>
menu.items.map((item) => ({ path: item.url, title: item.title })),
),
...SUB_MENU,
]

52
app/contexts/admin.tsx Normal file
View File

@ -0,0 +1,52 @@
import {
createContext,
useState,
useContext,
type PropsWithChildren,
type Dispatch,
type SetStateAction,
} from 'react'
import { z } from 'zod'
export const uploadCategorySchema = z
.enum(['featured_image', 'ads', 'content', 'profile_picture'])
.optional()
type TUpload = z.infer<typeof uploadCategorySchema>
type AdminContextProperties = {
isUploadOpen: TUpload
setIsUploadOpen: Dispatch<SetStateAction<TUpload>>
uploadedFile?: string
setUploadedFile: Dispatch<SetStateAction<string | undefined>>
}
const AdminContext = createContext<AdminContextProperties | undefined>(
undefined,
)
export const AdminProvider = ({ children }: PropsWithChildren) => {
const [isUploadOpen, setIsUploadOpen] = useState<TUpload>()
const [uploadedFile, setUploadedFile] = useState<string | undefined>()
return (
<AdminContext.Provider
value={{
isUploadOpen,
setIsUploadOpen,
uploadedFile,
setUploadedFile,
}}
>
{children}
</AdminContext.Provider>
)
}
export const useAdminContext = (): AdminContextProperties => {
const context = useContext(AdminContext)
if (!context) {
throw new Error('useAdminContext must be used within a AdminProvider')
}
return context
}

View File

@ -1,10 +1,15 @@
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
import type { PropsWithChildren } from 'react'
import { useAdminContext } from '~/contexts/admin'
import { FormUpload } from './form-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 />
@ -12,6 +17,28 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
<Sidebar />
<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>
</div>
)
}

View File

@ -0,0 +1,122 @@
import { Button } from '@headlessui/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState, type ChangeEvent } from 'react'
import { useFetcher } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { uploadCategorySchema, useAdminContext } from '~/contexts/admin'
export const uploadSchema = z.object({
file: z.instanceof(File),
category: uploadCategorySchema,
})
export type TUploadSchema = z.infer<typeof uploadSchema>
export const FormUpload = () => {
const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext()
const fetcher = useFetcher()
const [disabled, setDisabled] = useState(false)
const [error, setError] = useState<string>()
const maxFileSize = 1024 // 1MB
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)
setIsUploadOpen(undefined)
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/*"
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

@ -30,7 +30,7 @@ export const MENU: TMenu[] = [
icon: DocumentIcon,
},
{
title: 'Konten',
title: 'Artikel',
url: '/lg-admin/contents',
icon: ChatIcon,
},
@ -59,6 +59,38 @@ export const MENU: TMenu[] = [
url: '/lg-admin/tags',
icon: TagIcon,
},
{
title: 'Subscribe Plan',
url: '/lg-admin/subscribe-plan',
icon: TagIcon,
},
],
},
]
export const SUB_MENU = [
{
title: 'Buat Artikel',
path: '/lg-admin/contents/create',
},
{
title: 'Update Artikel',
path: '/lg-admin/contents/update',
},
{
title: 'Buat Kategori',
path: '/lg-admin/categories/create',
},
{
title: 'Update Kategori',
path: '/lg-admin/categories/update',
},
{
title: 'Buat Tag',
path: '/lg-admin/tags/create',
},
{
title: 'Update Tag',
path: '/lg-admin/tags/update',
},
]

View File

@ -1,5 +1,7 @@
import { Field, Input, Label, Select } from '@headlessui/react'
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import type { ConfigColumns } from 'datatables.net-dt'
import type { DataTableSlots } from 'datatables.net-react'
import { useState } from 'react'
import { twMerge } from 'tailwind-merge'
@ -36,14 +38,14 @@ export const AdvertisementsPage = ({
}
const dataBanner = BANNER
const dataColumns = [
const dataColumns: ConfigColumns[] = [
{ title: 'No', data: 'id' },
{ title: 'Banner', data: 'urlImage' },
{ title: 'Link', data: 'link' },
{ title: 'Tgl Create', data: 'createdAt' },
{ title: 'Status', data: 'status' },
]
const dataSlot = {
const dataSlot: DataTableSlots = {
1: (value: string) => {
return (
<div>

View File

@ -1,5 +1,5 @@
import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import DT, { type Config, type ConfigColumns } from 'datatables.net-dt'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { Link, useRouteLoaderData } from 'react-router'
import type { TCategoryResponse } from '~/apis/common/get-categories'
@ -13,17 +13,18 @@ export const CategoriesPage = () => {
)
DataTable.use(DT)
const dataTable = loaderData?.categoriesData?.sort((a, b) => {
if (a.sequence === null) return 1
if (b.sequence === null) return -1
return a.sequence - b.sequence
})
const dataColumns = [
const dataTable =
loaderData?.categoriesData?.sort((a, b) => {
if (a.sequence === null) return 1
if (b.sequence === null) return -1
return a.sequence - b.sequence
}) || []
const dataColumns: ConfigColumns[] = [
{
title: 'No',
render: (
data: unknown,
type: unknown,
_data: unknown,
_type: unknown,
row: TCategoryResponse,
meta: { row: number },
) => {
@ -46,7 +47,7 @@ export const CategoriesPage = () => {
data: 'id',
},
]
const dataSlot = {
const dataSlot: DataTableSlots = {
1: (_value: unknown, _type: unknown, data: TCategoryResponse) => (
<div>
<div>{data.name}</div>
@ -64,7 +65,7 @@ export const CategoriesPage = () => {
</Button>
),
}
const dataOptions = {
const dataOptions: Config = {
paging: true,
searching: true,
ordering: true,

View File

@ -1,5 +1,5 @@
import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import DT, { type Config, type ConfigColumns } from 'datatables.net-dt'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { Link, useRouteLoaderData } from 'react-router'
import type { TCategoryResponse } from '~/apis/common/get-categories'
@ -17,16 +17,17 @@ export const ContentsPage = () => {
)
DataTable.use(DT)
const dataTable = loaderData?.newsData?.sort(
(a, b) => new Date(b.live_at).getTime() - new Date(a.live_at).getTime(),
)
const dataColumns = [
const dataTable =
loaderData?.newsData?.sort(
(a, b) => new Date(b.live_at).getTime() - new Date(a.live_at).getTime(),
) || []
const dataColumns: ConfigColumns[] = [
{
title: 'No',
render: (
data: unknown,
type: unknown,
row: unknown,
_data: unknown,
_type: unknown,
_row: unknown,
meta: { row: number },
) => {
return meta.row + 1
@ -54,7 +55,7 @@ export const ContentsPage = () => {
data: 'slug',
},
]
const dataSlot = {
const dataSlot: DataTableSlots = {
1: (value: string) => formatDate(value),
2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
<div>
@ -62,6 +63,7 @@ export const ContentsPage = () => {
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
</div>
),
3: (value: string) => <span className="text-sm">{value}</span>,
4: (value: TCategoryResponse[]) => (
<div className="text-xs">{value.map((item) => item.name).join(', ')}</div>
),
@ -89,7 +91,7 @@ export const ContentsPage = () => {
</Button>
),
}
const dataOptions = {
const dataOptions: Config = {
paging: true,
searching: true,
ordering: true,
@ -98,7 +100,7 @@ export const ContentsPage = () => {
return (
<div className="relative">
<TitleDashboard title="Konten" />
<TitleDashboard title="Artikel" />
<div className="mb-8 flex items-end justify-between gap-5">
<div className="flex-1">{/* TODO: Filter */}</div>
<Button
@ -116,7 +118,7 @@ export const ContentsPage = () => {
columns={dataColumns}
slots={dataSlot}
options={dataOptions}
title="Daftar Konten"
title="Daftar Artikel"
/>
</div>
)

View File

@ -0,0 +1,83 @@
import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import { Link, useRouteLoaderData } from 'react-router'
import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard'
import type { loader } from '~/routes/_admin.lg-admin._dashboard.subscribe-plan._index'
export const SubscribePlanPage = () => {
const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.subscribe-plan._index',
)
DataTable.use(DT)
const { subscriptionsData: dataTable } = loaderData || {}
const dataColumns = [
{
title: 'No',
render: (
_data: unknown,
_type: unknown,
_row: unknown,
meta: { row: number },
) => {
return meta.row + 1
},
},
{
title: 'Nama',
data: 'name',
},
{
title: 'Kode',
data: 'code',
},
{
title: 'Action',
data: 'id',
},
]
const dataSlot = {
3: (value: string) => (
<Button
as="a"
href={`/lg-admin/subscribe-plan/update/${value}`}
className="text-md rounded-md"
size="sm"
>
Update Subscribe Plan
</Button>
),
}
return (
<div className="relative">
<TitleDashboard title="Subscribe Plan" />
<div className="mb-8 flex items-end justify-between">
<div className="flex-1">{/* TODO: Filter */}</div>
<Button
as={Link}
to="/lg-admin/subscribe-plan/create"
className="text-md h-[42px] rounded-md"
size="lg"
>
Buat Subscribe Plan
</Button>
</div>
<UiTable
data={dataTable || []}
columns={dataColumns}
slots={dataSlot}
options={{
paging: true,
searching: true,
ordering: true,
info: true,
}}
title=" Daftar Subscribe Plan"
/>
</div>
)
}

View File

@ -8,15 +8,6 @@ type TSubscriptions = {
status: string
}
type TSubSettings = {
id: number
createdAt: string
subscriber: 'Monthly' | 'Yearly' | 'Weekly' | 'Special'
price: string
length: '30' | '365' | '7' | '1'
status: string
}
export const SUBSCRIPTIONS: TSubscriptions[] = [
{
id: 1,
@ -199,86 +190,3 @@ export const SUBSCRIPTIONS: TSubscriptions[] = [
status: 'Draft',
},
]
export const SUBSETTINGS: TSubSettings[] = [
{
id: 1,
createdAt: '24/10/2024',
subscriber: 'Monthly',
price: '100',
length: '30',
status: 'active',
},
{
id: 2,
createdAt: '25/10/2024',
subscriber: 'Yearly',
price: '1000',
length: '365',
status: 'active',
},
{
id: 3,
createdAt: '26/10/2024',
subscriber: 'Weekly',
price: '25',
length: '7',
status: 'inactive',
},
{
id: 4,
createdAt: '27/10/2024',
subscriber: 'Monthly',
price: '100',
length: '30',
status: 'active',
},
{
id: 5,
createdAt: '28/10/2024',
subscriber: 'Yearly',
price: '1000',
length: '365',
status: 'inactive',
},
{
id: 6,
createdAt: '29/10/2024',
subscriber: 'Weekly',
price: '25',
length: '7',
status: 'active',
},
{
id: 7,
createdAt: '30/10/2024',
subscriber: 'Monthly',
price: '100',
length: '30',
status: 'inactive',
},
{
id: 8,
createdAt: '31/10/2024',
subscriber: 'Yearly',
price: '1000',
length: '365',
status: 'active',
},
{
id: 9,
createdAt: '01/11/2024',
subscriber: 'Weekly',
price: '25',
length: '7',
status: 'inactive',
},
{
id: 10,
createdAt: '02/11/2024',
subscriber: 'Monthly',
price: '100',
length: '30',
status: 'active',
},
]

View File

@ -1,18 +1,14 @@
import { Field, Input, Label, Select } from '@headlessui/react'
import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import { useState } from 'react'
import { SearchIcon } from '~/components/icons/search'
import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard'
import { SUBSCRIPTIONS, SUBSETTINGS } from './data'
import { SUBSCRIPTIONS } from './data'
export const SubscriptionsPage = () => {
const [SubscribtionOpen, setSubscribtionOpen] = useState(true)
// const [SubSettingOpen, setSubSettingOpen] = useState(false)
DataTable.use(DT)
const colTableSubscription = [
{ title: 'No', data: 'id' },
@ -28,158 +24,55 @@ export const SubscriptionsPage = () => {
},
},
]
const colTableSubSetting = [
{ title: 'No', data: 'id' },
{ title: 'Tanggal Pembuatan', data: 'createdAt' },
{ title: 'Nama Subscription', data: 'subscriber' },
{ title: 'Price', data: 'price' },
{ title: 'Length', data: 'length' },
{
title: 'Status',
data: 'status',
render: (value: string) => {
return value == 'active'
? `<span class="bg-[#DFE5FF] text-[#4C5CA0] px-2 py-1 rounded-md">${value}</span>`
: `<span class="bg-[#FFD1D1] text-[#FF4E4E] px-2 py-1 rounded-md">${value}</span>`
},
},
]
const switchView = () => {
setSubscribtionOpen(!SubscribtionOpen)
}
return (
<div className="relative">
<TitleDashboard title="Subscription" />
{SubscribtionOpen && (
<>
<div className="mb-8 flex items-end justify-between">
<div className="flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="w-[400px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Cari User
</Label>
<div className="relative">
<Input
type="text"
placeholder="Cari Nama"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<SearchIcon className="h-5 w-5" />
</div>
</div>
</Field>
<div className="mb-8 flex items-end justify-between">
<div className="flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="w-[400px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Cari User
</Label>
<div className="relative">
<Input
type="text"
placeholder="Cari Nama"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<SearchIcon className="h-5 w-5" />
</div>
</div>
<div className="w-[235px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Status
</Label>
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none">
<option>Pilih Status</option>
<option>Aktif</option>
<option>Nonaktif</option>
</Select>
</Field>
</div>
</div>
<Button
className="rounded-md"
size="lg"
onClick={switchView}
>
Subscription Settings
</Button>
</Field>
</div>
<UiTable
data={SUBSCRIPTIONS}
columns={colTableSubscription}
options={{
paging: true,
searching: true,
ordering: true,
info: true,
}}
title="Daftar Subscription"
/>
</>
)}
{!SubscribtionOpen && (
<>
<div className="mb-8 flex items-end justify-between">
<div className="flex items-end gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="w-[300px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Subscription Name
</Label>
<div className="relative">
<Input
type="text"
placeholder="Subscription Name"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
</div>
</Field>
</div>
<div className="w-[300px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Subscription Price
</Label>
<div className="relative">
<Input
type="text"
placeholder="Subscription Price"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
</div>
</Field>
</div>
<div className="w-[300px]">
<Field>
<Label className="mb-2 block text-sm font-medium">
Subscription Length (Days)
</Label>
<Input
type="text"
placeholder="Subscription Length (Days)"
className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
></Input>
</Field>
</div>
</div>
<Button
className="rounded-md"
size="lg"
onClick={switchView}
>
Save
</Button>
<div className="w-[235px]">
<Field>
<Label className="mb-2 block text-sm font-medium">Status</Label>
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none">
<option>Pilih Status</option>
<option>Aktif</option>
<option>Nonaktif</option>
</Select>
</Field>
</div>
</div>
</div>
<UiTable
data={SUBSETTINGS}
columns={colTableSubSetting}
options={{
paging: true,
searching: true,
ordering: true,
info: true,
}}
title=" Daftar Subscription"
/>
</>
)}
<UiTable
data={SUBSCRIPTIONS}
columns={colTableSubscription}
options={{
paging: true,
searching: true,
ordering: true,
info: true,
}}
title="Daftar Subscription"
/>
</div>
)
}

View File

@ -1,5 +1,5 @@
import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react'
import DT, { type Config, type ConfigColumns } from 'datatables.net-dt'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { Link, useRouteLoaderData } from 'react-router'
import { Button } from '~/components/ui/button'
@ -14,13 +14,13 @@ export const TagsPage = () => {
const { tagsData: dataTable } = loaderData || {}
DataTable.use(DT)
const dataColumns = [
const dataColumns: ConfigColumns[] = [
{
title: 'No',
render: (
data: unknown,
type: unknown,
row: unknown,
_data: unknown,
_type: unknown,
_row: unknown,
meta: { row: number },
) => {
return meta.row + 1
@ -39,15 +39,7 @@ export const TagsPage = () => {
data: 'id',
},
]
const dataOptions = {
paging: true,
searching: true,
ordering: true,
info: true,
}
const dataSlot = {
const dataSlot: DataTableSlots = {
3: (value: string) => (
<Button
as="a"
@ -59,6 +51,13 @@ export const TagsPage = () => {
</Button>
),
}
const dataOptions: Config = {
paging: true,
searching: true,
ordering: true,
info: true,
}
return (
<div className="relative">
<TitleDashboard title="Tags" />
@ -75,7 +74,7 @@ export const TagsPage = () => {
</div>
<UiTable
data={dataTable}
data={dataTable || []}
columns={dataColumns}
options={dataOptions}
slots={dataSlot}

View File

@ -1,56 +0,0 @@
type TUsers = {
id: number
idTransaction: number
date: string
name: string
email: string
category: string
status: string
}
export const USERS: TUsers[] = [
{
id: 1,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Baru',
},
{
id: 2,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Premium',
},
{
id: 3,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Pembayaran',
},
{
id: 4,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Premium',
},
{
id: 5,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Baru',
},
]

View File

@ -1,110 +1,98 @@
import { Field, Input, Label, Select } from '@headlessui/react'
import DT, { type ConfigColumns } from 'datatables.net-dt'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { useRouteLoaderData } from 'react-router'
import { SearchIcon } from '~/components/icons/search'
import { Pagination } from '~/components/ui/pagination'
import type { TUserResponse } from '~/apis/admin/get-users'
import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard'
import { USERS } from './data'
import type { loader } from '~/routes/_admin.lg-admin._dashboard.users._index'
import { formatDate } from '~/utils/formatter'
type TColorBadge = 'Baru' | 'Premium' | 'Pembayaran'
const getStatusBadge = (status: TColorBadge) => {
const statusColors = {
Baru: 'bg-[#DFE5FF] text-[#4C5CA0]',
Premium: 'bg-[#FFFCAF] text-[#DBCA6E]',
Pembayaran: 'bg-[#FEC4FF] text-[#CC6EDB]',
}
return statusColors[status] || 'bg-gray-200 text-gray-700'
}
export const UsersPage = () => {
const getStatusBadge = (status: TColorBadge) => {
const statusColors = {
Baru: 'bg-[#DFE5FF] text-[#4C5CA0]',
Premium: 'bg-[#FFFCAF] text-[#DBCA6E]',
Pembayaran: 'bg-[#FEC4FF] text-[#CC6EDB]',
}
return statusColors[status] || 'bg-gray-200 text-gray-700'
const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.users._index',
)
DataTable.use(DT)
const dataTable =
loaderData?.usersData?.sort(
(a, b) =>
new Date(b.subscribe.start_date).getTime() -
new Date(a.subscribe.start_date).getTime(),
) || []
const dataColumns: ConfigColumns[] = [
{
title: 'No',
render: (
_data: unknown,
_type: unknown,
_row: unknown,
meta: { row: number },
) => {
return meta.row + 1
},
},
{
title: 'Tanggal Daftar',
data: 'subscribe.start_date',
},
{
title: 'Nama User',
},
{
title: 'Email',
data: 'email',
},
{
title: 'Kategori',
data: 'subscribe.status',
},
{
title: 'Status',
data: 'subscribe.subscribe_plan.name',
},
]
const dataSlot: DataTableSlots = {
1: (value: string) => formatDate(value),
2: (_value: unknown, _type: unknown, data: TUserResponse) => (
<div>
<div>{data.phone}</div>
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
</div>
),
4: (_value: string) => <span className="text-sm">Pribadi</span>,
5: (value: TColorBadge) => (
<span className={`rounded-lg px-2 text-sm ${getStatusBadge(value)}`}>
{value}
</span>
),
}
return (
<div className="relative">
<TitleDashboard title="Users" />
{/* filter section */}
<div className="mb-8 flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="w-[400px]">
<Field>
<Label className="mb-2 block text-sm font-medium">Cari User</Label>
<div className="relative">
<Input
type="text"
placeholder="Cari Nama"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<SearchIcon className="h-5 w-5" />
</div>
</div>
</Field>
</div>
<div className="w-[235px]">
<Field>
<Label className="mb-2 block text-sm font-medium">Status</Label>
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none">
<option>Pilih Status</option>
<option>Aktif</option>
<option>Nonaktif</option>
</Select>
</Field>
</div>
<div className="mb-8 flex items-end justify-between gap-5">
<div className="flex-1">{/* TODO: Filter */}</div>
</div>
{/* table */}
<div className="overflow-x-auto">
<div className="rounded-lg bg-white px-6 py-8 shadow-sm">
<h2 className="text-xl font-bold text-[#4C5CA0]">Daftar User</h2>
<table className="min-w-full p-3">
<thead className="my-5 border-b-3 border-[#C2C2C2]">
<tr>
<th className="p-3">No</th>
<th>Tanggal Daftar</th>
<th>Nama User</th>
<th>Email</th>
<th>Kategori</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{USERS.map((user, index) => (
<tr
key={user.id}
className="border-b-1 border-gray-200 text-center"
>
<td className="p-4">{index + 1}</td>
<td>{user.date}</td>
<td>
{user.name}
<div className="text-sm text-gray-500">
id: {user.idTransaction}
</div>
</td>
<td>{user.email}</td>
<td>{user.category}</td>
<td className="">
<span
className={`inline-block min-w-[100px] rounded-full px-2 py-1 text-sm ${getStatusBadge(user.status as TColorBadge)}`}
>
{user.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* pagination */}
<div className="float-end mt-6">
<Pagination
currentPage={1}
totalPages={5}
onPageChange={() => {}}
/>
</div>
</div>
<UiTable
data={dataTable || []}
columns={dataColumns}
slots={dataSlot}
title="Daftar Users"
/>
</div>
)
}

View File

@ -10,6 +10,7 @@ import { TextEditor } from '~/components/text-editor'
import { Button } from '~/components/ui/button'
import { Combobox } from '~/components/ui/combobox'
import { Input } from '~/components/ui/input'
import { InputFile } from '~/components/ui/input-file'
import { Switch } from '~/components/ui/switch'
import { TitleDashboard } from '~/components/ui/title-dashboard'
import type { loader } from '~/routes/_admin.lg-admin._dashboard'
@ -133,10 +134,10 @@ export const FormContentsPage = (properties: TProperties) => {
containerClassName="flex-1"
disabled={!!newsData}
/>
<Input
<InputFile
id="featured_image"
label="Gambar Unggulan"
placeholder="Masukkan Gambar Unggulan"
placeholder="Masukkan Url Gambar Unggulan"
name="featured_image"
className="border-0 bg-white shadow read-only:bg-gray-100 focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none disabled:bg-gray-100"
labelClassName="text-sm font-medium text-[#363636]"
@ -207,7 +208,7 @@ export const FormContentsPage = (properties: TProperties) => {
label="Konten"
placeholder="Masukkan Konten"
className="shadow"
inputClassName="bg-white focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none border-0"
inputClassName="bg-white focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none border-0 min-h-[42px]"
labelClassName="text-sm font-medium text-[#363636]"
category="content"
/>

View File

@ -29,9 +29,7 @@ export const NewsDetailPage = () => {
<div className="sm-max:mx-5 relative">
<Card>
<div className="py-5 sm:px-30">
<h2 className="text-xl font-extrabold text-[#2E2F7C] sm:text-4xl">
{title}
</h2>
<h2 className="text-xl font-extrabold sm:text-4xl">{title}</h2>
<div className="my-5 w-full items-center justify-between gap-2 align-middle sm:flex">
<NewsAuthor

View File

@ -0,0 +1,11 @@
import { getSubscriptions } from '~/apis/common/get-subscriptions'
import { SubscribePlanPage } from '~/pages/dashboard-plan-subscribe'
import type { Route } from './+types/_admin.lg-admin._dashboard.contents._index'
export const loader = async ({}: Route.LoaderArgs) => {
const { data: subscriptionsData } = await getSubscriptions()
return { subscriptionsData }
}
const DashboardSubscriptionsSettingsLayout = () => <SubscribePlanPage />
export default DashboardSubscriptionsSettingsLayout

View File

@ -2,6 +2,7 @@ import { Outlet } from 'react-router'
import { getCategories } from '~/apis/common/get-categories'
import { getTags } from '~/apis/common/get-tags'
import { AdminProvider } from '~/contexts/admin'
import { AdminDashboardLayout } from '~/layouts/admin/dashboard'
import type { Route } from './+types/_admin.lg-admin._dashboard'
@ -18,9 +19,11 @@ export const loader = async ({}: Route.LoaderArgs) => {
const DashboardLayout = () => {
return (
<AdminDashboardLayout>
<Outlet />
</AdminDashboardLayout>
<AdminProvider>
<AdminDashboardLayout>
<Outlet />
</AdminDashboardLayout>
</AdminProvider>
)
}
export default DashboardLayout

View File

@ -0,0 +1,17 @@
import { getUsers } from '~/apis/admin/get-users'
import { handleCookie } from '~/libs/cookies'
import { UsersPage } from '~/pages/dashboard-users'
import type { Route } from './+types/_admin.lg-admin._dashboard.users._index'
export const loader = async ({ request }: Route.LoaderArgs) => {
const { staffToken } = await handleCookie(request)
const { data: usersData } = await getUsers({
accessToken: staffToken,
})
return { usersData }
}
const DashboardUsersLayout = () => <UsersPage />
export default DashboardUsersLayout

View File

@ -1,4 +0,0 @@
import { UsersPage } from '~/pages/dashboard-users'
const DashboardUsersLayout = () => <UsersPage />
export default DashboardUsersLayout

View File

@ -1,9 +1,11 @@
import { Outlet, redirect } from 'react-router'
import { XiorError } from 'xior'
import { getStaff } from '~/apis/admin/get-staff'
import { AUTH_PAGES } from '~/configs/pages'
import { AdminDefaultLayout } from '~/layouts/admin/default'
import { handleCookie } from '~/libs/cookies'
import { setStaffLogoutHeaders } from '~/libs/logout-header.server'
import type { Route } from './+types/_admin.lg-admin'
@ -13,6 +15,19 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
const isAuthPage = AUTH_PAGES.includes(pathname)
let staffData
if (staffToken) {
try {
const { data } = await getStaff({
accessToken: staffToken,
})
staffData = data
} catch (error) {
if (error instanceof XiorError && error.response?.status === 401) {
setStaffLogoutHeaders()
}
}
}
if (!isAuthPage && !staffToken) {
throw redirect('/lg-admin/login')
}
@ -21,13 +36,6 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
throw redirect('/lg-admin')
}
if (staffToken) {
const { data } = await getStaff({
accessToken: staffToken,
})
staffData = data
}
return {
staffData,
}

View File

@ -1,4 +1,5 @@
import { Outlet } from 'react-router'
import { XiorError } from 'xior'
import { getCategories } from '~/apis/common/get-categories'
import { getSubscriptions } from '~/apis/common/get-subscriptions'
@ -6,6 +7,7 @@ import { getUser } from '~/apis/news/get-user'
import { NewsProvider } from '~/contexts/news'
import { NewsDefaultLayout } from '~/layouts/news/default'
import { handleCookie } from '~/libs/cookies'
import { setUserLogoutHeaders } from '~/libs/logout-header.server'
import type { Route } from './+types/_news'
@ -13,10 +15,16 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
const { userToken } = await handleCookie(request)
let userData
if (userToken) {
const { data } = await getUser({
accessToken: userToken,
})
userData = data
try {
const { data } = await getUser({
accessToken: userToken,
})
userData = data
} catch (error) {
if (error instanceof XiorError && error.response?.status === 401) {
setUserLogoutHeaders()
}
}
}
const { data: subscriptionsData } = await getSubscriptions()
const { data: categoriesData } = await getCategories()

View File

@ -0,0 +1,64 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { data } from 'react-router'
import { getValidatedFormData } from 'remix-hook-form'
import { XiorError } from 'xior'
import { uploadFileRequest } from '~/apis/admin/upload-file'
import { uploadSchema, type TUploadSchema } from '~/layouts/admin/form-upload'
import { handleCookie } from '~/libs/cookies'
import type { Route } from './+types/actions.register'
export const action = async ({ request }: Route.ActionArgs) => {
const { staffToken } = await handleCookie(request)
try {
const {
errors,
data: payload,
receivedValues: defaultValues,
} = await getValidatedFormData<TUploadSchema>(
request,
zodResolver(uploadSchema),
false,
)
if (errors) {
return data({ success: false, errors, defaultValues }, { status: 400 })
}
const { data: uploadData } = await uploadFileRequest({
payload,
accessToken: staffToken,
})
return data(
{
success: true,
uploadData,
},
{
status: 200,
statusText: 'OK',
},
)
} catch (error) {
if (error instanceof XiorError) {
return data(
{
success: false,
message: error?.response?.data?.error?.message || error.message,
},
{
status: error?.response?.status || 500,
},
)
}
return data(
{
success: false,
message: 'Internal server error',
},
{ status: 500 },
)
}
}