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

This commit is contained in:
fredy.siswanto 2025-03-09 23:25:01 +07:00
commit 8c2298ff61
8 changed files with 115 additions and 18 deletions

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: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' : '', isActive ? 'bg-[#2E2F7C]/10' : '',
className, className,
)} )}

View File

@ -11,6 +11,8 @@ import {
import { useRemixFormContext } from 'remix-hook-form' import { useRemixFormContext } from 'remix-hook-form'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { useAdminContext } from '~/contexts/admin'
import { Button } from './button' import { Button } from './button'
type TInputProperties<T extends FieldValues> = Omit< type TInputProperties<T extends FieldValues> = Omit<
@ -40,6 +42,7 @@ export const InputFile = <TFormValues extends Record<string, unknown>>(
labelClassName, labelClassName,
...restProperties ...restProperties
} = properties } = properties
const { setIsUploadOpen } = useAdminContext()
const { const {
register, register,
@ -59,7 +62,7 @@ export const InputFile = <TFormValues extends Record<string, unknown>>(
</Label> </Label>
<HeadlessInput <HeadlessInput
className={twMerge( 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, className,
)} )}
placeholder={placeholder} placeholder={placeholder}
@ -71,7 +74,9 @@ export const InputFile = <TFormValues extends Record<string, unknown>>(
variant="icon" variant="icon"
size="fit" size="fit"
className="absolute right-3 h-[42px]" className="absolute right-3 h-[42px]"
onClick={() => {}} onClick={() => {
setIsUploadOpen('featured_image')
}}
> >
<CloudArrowUpIcon className="h-4 w-4 text-gray-500/50" /> <CloudArrowUpIcon className="h-4 w-4 text-gray-500/50" />
</Button> </Button>

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', 'h-[42px] w-full rounded-md border border-[#DFDFDF] p-2 pr-8',
className, className,
)} )}
placeholder={inputType === 'password' ? '******' : placeholder} placeholder={inputType === 'password' ? '******' : placeholder}

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

@ -0,0 +1,47 @@
import {
createContext,
useState,
useContext,
type PropsWithChildren,
type Dispatch,
type SetStateAction,
} from 'react'
type TUpload =
| 'featured_image'
| 'ads'
| 'content'
| 'profile_picture'
| undefined
type AdminContextProperties = {
isUploadOpen: TUpload
setIsUploadOpen: Dispatch<SetStateAction<TUpload>>
}
const AdminContext = createContext<AdminContextProperties | undefined>(
undefined,
)
export const AdminProvider = ({ children }: PropsWithChildren) => {
const [isUploadOpen, setIsUploadOpen] = useState<TUpload>()
return (
<AdminContext.Provider
value={{
isUploadOpen,
setIsUploadOpen,
}}
>
{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,14 @@
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import { useAdminContext } from '~/contexts/admin'
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 />
@ -12,6 +16,28 @@ 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"
>
Upload di mari {isUploadOpen}
</DialogPanel>
</div>
</Dialog>
</div> </div>
) )
} }

View File

@ -2,6 +2,7 @@ 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'
@ -18,9 +19,11 @@ 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,9 +1,11 @@
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'
@ -13,6 +15,19 @@ 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')
} }
@ -21,13 +36,6 @@ 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,4 +1,5 @@
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'
@ -6,6 +7,7 @@ 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'
@ -13,10 +15,16 @@ 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()