Compare commits

..

No commits in common. "9a0c6c1f0b667c51dac2e984d4da21ff90b716f3" and "e7eda086e42016847d9d1c943d6accbcc9e5d15b" have entirely different histories.

33 changed files with 564 additions and 798 deletions

View File

@ -1,39 +0,0 @@
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

@ -1,37 +0,0 @@
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

@ -0,0 +1,28 @@
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} 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 hover:bg-[#2E2F7C] hover:text-white disabled:cursor-not-allowed disabled:bg-[#2E2F7C]/50 disabled:text-white disabled:hover:bg-[#2E2F7C]/50',
isActive ? 'bg-[#2E2F7C]/10' : '', isActive ? 'bg-[#2E2F7C]/10' : '',
className, className,
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,122 +0,0 @@
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, icon: DocumentIcon,
}, },
{ {
title: 'Artikel', title: 'Konten',
url: '/lg-admin/contents', url: '/lg-admin/contents',
icon: ChatIcon, icon: ChatIcon,
}, },
@ -59,38 +59,6 @@ export const MENU: TMenu[] = [
url: '/lg-admin/tags', url: '/lg-admin/tags',
icon: TagIcon, 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,7 +1,5 @@
import { Field, Input, Label, Select } from '@headlessui/react' import { Field, Input, Label, Select } from '@headlessui/react'
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' 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 { useState } from 'react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
@ -38,14 +36,14 @@ export const AdvertisementsPage = ({
} }
const dataBanner = BANNER const dataBanner = BANNER
const dataColumns: ConfigColumns[] = [ const dataColumns = [
{ title: 'No', data: 'id' }, { title: 'No', data: 'id' },
{ title: 'Banner', data: 'urlImage' }, { title: 'Banner', data: 'urlImage' },
{ title: 'Link', data: 'link' }, { title: 'Link', data: 'link' },
{ title: 'Tgl Create', data: 'createdAt' }, { title: 'Tgl Create', data: 'createdAt' },
{ title: 'Status', data: 'status' }, { title: 'Status', data: 'status' },
] ]
const dataSlot: DataTableSlots = { const dataSlot = {
1: (value: string) => { 1: (value: string) => {
return ( return (
<div> <div>

View File

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

View File

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

View File

@ -1,83 +0,0 @@
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,6 +8,15 @@ type TSubscriptions = {
status: string 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[] = [ export const SUBSCRIPTIONS: TSubscriptions[] = [
{ {
id: 1, id: 1,
@ -190,3 +199,86 @@ export const SUBSCRIPTIONS: TSubscriptions[] = [
status: 'Draft', 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,14 +1,18 @@
import { Field, Input, Label, Select } from '@headlessui/react' import { Field, Input, Label, Select } from '@headlessui/react'
import DT from 'datatables.net-dt' import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react' import DataTable from 'datatables.net-react'
import { useState } from 'react'
import { SearchIcon } from '~/components/icons/search' import { SearchIcon } from '~/components/icons/search'
import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table' import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard' import { TitleDashboard } from '~/components/ui/title-dashboard'
import { SUBSCRIPTIONS } from './data' import { SUBSCRIPTIONS, SUBSETTINGS } from './data'
export const SubscriptionsPage = () => { export const SubscriptionsPage = () => {
const [SubscribtionOpen, setSubscribtionOpen] = useState(true)
// const [SubSettingOpen, setSubSettingOpen] = useState(false)
DataTable.use(DT) DataTable.use(DT)
const colTableSubscription = [ const colTableSubscription = [
{ title: 'No', data: 'id' }, { title: 'No', data: 'id' },
@ -24,55 +28,158 @@ 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 ( return (
<div className="relative"> <div className="relative">
<TitleDashboard title="Subscription" /> <TitleDashboard title="Subscription" />
<div className="mb-8 flex items-end justify-between"> {SubscribtionOpen && (
<div className="flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]"> <>
<div className="w-[400px]"> <div className="mb-8 flex items-end justify-between">
<Field> <div className="flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<Label className="mb-2 block text-sm font-medium"> <div className="w-[400px]">
Cari User <Field>
</Label> <Label className="mb-2 block text-sm font-medium">
<div className="relative"> Cari User
<Input </Label>
type="text" <div className="relative">
placeholder="Cari Nama" <Input
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" type="text"
/> placeholder="Cari Nama"
<div className="absolute inset-y-0 right-0 flex items-center pr-3"> className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
<SearchIcon className="h-5 w-5" /> />
</div> <div className="absolute inset-y-0 right-0 flex items-center pr-3">
<SearchIcon className="h-5 w-5" />
</div>
</div>
</Field>
</div> </div>
</Field>
<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>
</div> </div>
<div className="w-[235px]"> <UiTable
<Field> data={SUBSCRIPTIONS}
<Label className="mb-2 block text-sm font-medium">Status</Label> columns={colTableSubscription}
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"> options={{
<option>Pilih Status</option> paging: true,
<option>Aktif</option> searching: true,
<option>Nonaktif</option> ordering: true,
</Select> info: true,
</Field> }}
</div> title="Daftar Subscription"
</div> />
</div> </>
)}
<UiTable {!SubscribtionOpen && (
data={SUBSCRIPTIONS} <>
columns={colTableSubscription} <div className="mb-8 flex items-end justify-between">
options={{ <div className="flex items-end gap-5 rounded-lg bg-gray-50 text-[#363636]">
paging: true, <div className="w-[300px]">
searching: true, <Field>
ordering: true, <Label className="mb-2 block text-sm font-medium">
info: true, Subscription Name
}} </Label>
title="Daftar Subscription" <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>
<UiTable
data={SUBSETTINGS}
columns={colTableSubSetting}
options={{
paging: true,
searching: true,
ordering: true,
info: true,
}}
title=" Daftar Subscription"
/>
</>
)}
</div> </div>
) )
} }

View File

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

View File

@ -0,0 +1,56 @@
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,98 +1,110 @@
import DT, { type ConfigColumns } from 'datatables.net-dt' import { Field, Input, Label, Select } from '@headlessui/react'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { useRouteLoaderData } from 'react-router'
import type { TUserResponse } from '~/apis/admin/get-users' import { SearchIcon } from '~/components/icons/search'
import { UiTable } from '~/components/ui/table' import { Pagination } from '~/components/ui/pagination'
import { TitleDashboard } from '~/components/ui/title-dashboard' import { TitleDashboard } from '~/components/ui/title-dashboard'
import type { loader } from '~/routes/_admin.lg-admin._dashboard.users._index'
import { formatDate } from '~/utils/formatter' import { USERS } from './data'
type TColorBadge = 'Baru' | 'Premium' | 'Pembayaran' 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 = () => { export const UsersPage = () => {
const loaderData = useRouteLoaderData<typeof loader>( const getStatusBadge = (status: TColorBadge) => {
'routes/_admin.lg-admin._dashboard.users._index', const statusColors = {
) Baru: 'bg-[#DFE5FF] text-[#4C5CA0]',
Premium: 'bg-[#FFFCAF] text-[#DBCA6E]',
DataTable.use(DT) Pembayaran: 'bg-[#FEC4FF] text-[#CC6EDB]',
const dataTable = }
loaderData?.usersData?.sort( return statusColors[status] || 'bg-gray-200 text-gray-700'
(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 ( return (
<div className="relative"> <div className="relative">
<TitleDashboard title="Users" /> <TitleDashboard title="Users" />
{/* filter section */}
<div className="mb-8 flex items-end justify-between gap-5"> <div className="mb-8 flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="flex-1">{/* TODO: Filter */}</div> <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> </div>
<UiTable {/* table */}
data={dataTable || []} <div className="overflow-x-auto">
columns={dataColumns} <div className="rounded-lg bg-white px-6 py-8 shadow-sm">
slots={dataSlot} <h2 className="text-xl font-bold text-[#4C5CA0]">Daftar User</h2>
title="Daftar Users" <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>
</div> </div>
) )
} }

View File

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

View File

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

View File

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

View File

@ -1,17 +0,0 @@
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

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

View File

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

View File

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

View File

@ -1,64 +0,0 @@
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 },
)
}
}