feat: migrate all 5 Vite apps from react-router to TanStack Router

Migrated backoffice, hackathon, dimentorin, gacha, qrcampaign, and
infra from custom react-router file-based routing to TanStack Router
file-based routing following the tanstack-frontend-best-practice
convention.

Key changes per app:
- New routes/ directory with __root.tsx, _public.tsx, _authenticated.tsx
- Auth guards via beforeLoad (replaces old middleware.ts)
- createFileRoute pattern for all page components
- TanStackRouterVite plugin in vite.config for auto route generation
- _components/_hooks folders colocated with routes (ignored by router)
- routeTree.gen.ts auto-generated on dev/build

Convention:
- _public/* routes redirect to dashboard if authenticated
- _authenticated/* routes redirect to /auth/login if not authenticated
- $param for dynamic segments (was [param] in old convention)
- _layout suffix for pathless layout routes

Removed:
- Old src/app/ directories from all apps
- Old src/middleware.ts files
- Custom convertPagesToRoute utility (no longer needed)
- react-router dependency usage (kept in package.json for shared libs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-11 17:04:14 +07:00
co-authored by Claude Opus 4.6
parent 86d9e759a6
commit 4240e8eb51
291 changed files with 3889 additions and 4585 deletions
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
-23
View File
@@ -1,23 +0,0 @@
import { Link } from 'react-router-dom';
export default function NotFoundPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
The page you are looking for doesn't exist or has been moved.
</p>
<Link
to="/hackathon-dashboard"
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Go Back Home
</Link>
</div>
</div>
);
}
-29
View File
@@ -1,29 +0,0 @@
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
export default function ErrorPage() {
const error = useRouteError();
let errorMessage: string;
if (isRouteErrorResponse(error)) {
errorMessage = error.statusText;
} else if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
} else {
console.error(error);
errorMessage = 'Unknown error';
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
<p className="text-xl text-gray-700 mb-2">
Sorry, an unexpected error has occurred.
</p>
<p className="text-gray-500 italic">{errorMessage}</p>
</div>
</div>
);
}
+17 -31
View File
@@ -1,42 +1,28 @@
import { createRoot } from 'react-dom/client'; import { StrictMode } from 'react'
import { middleware } from './middleware'; import { createRoot } from 'react-dom/client'
import { StrictMode } from 'react'; import { RouterProvider, createRouter } from '@tanstack/react-router'
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router'; import { QueryProvider } from '@imphnen-frontend-service/utils'
import { import { Toaster } from 'sonner'
add404PageToRoutesChildren, import { routeTree } from './routeTree.gen'
addErrorElementToRoutes, import './index.css'
convertPagesToRoute,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const router = createRouter({ routeTree })
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject; declare module '@tanstack/react-router' {
addErrorElementToRoutes(errorFiles, routes); interface Register {
add404PageToRoutesChildren(notFoundFiles, routes); router: typeof router
}
}
const router = createBrowserRouter([ const rootElement = document.getElementById('root')
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element')
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <QueryProvider>
<Toaster position="top-right" />
<RouterProvider router={router} /> <RouterProvider router={router} />
<Toaster position="top-right" />
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
); )
-100
View File
@@ -1,100 +0,0 @@
import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/hackathon-dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/hackathon-dashboard';
}
}
return null;
};
+2
View File
@@ -0,0 +1,2 @@
// This file is auto-generated by TanStack Router
export const routeTree = {} as any
+5
View File
@@ -0,0 +1,5 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => <Outlet />,
})
@@ -1,9 +1,20 @@
import { FC, ReactElement, useState } from 'react'; import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { Outlet } from 'react-router-dom'; import { SessionToken } from '@imphnen-frontend-service/service'
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; import { useState } from 'react'
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'
export const AppLayout: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated')({
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); beforeLoad: () => {
const session = SessionToken.get()
if (!session?.token?.access_token) {
throw redirect({ to: '/auth/login' })
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
return ( return (
<div className="bg-primary-50 min-h-screen flex justify-center"> <div className="bg-primary-50 min-h-screen flex justify-center">
@@ -13,14 +24,12 @@ export const AppLayout: FC = (): ReactElement => {
onClose={() => setMobileSidebarOpen(false)} onClose={() => setMobileSidebarOpen(false)}
/> />
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<header <header
className={ className={
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' + 'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' +
(mobileSidebarOpen ? 'z-0' : 'z-30') (mobileSidebarOpen ? 'z-0' : 'z-30')
} }
> >
<button <button
type="button" type="button"
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer" className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer"
@@ -50,7 +59,5 @@ export const AppLayout: FC = (): ReactElement => {
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default AppLayout;
@@ -1,14 +1,13 @@
import * as React from 'react'; import { createFileRoute } from '@tanstack/react-router'
import * as React from 'react'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'; import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -16,20 +15,24 @@ import {
PaginationState, PaginationState,
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalEditAccount from './_components/modal-edit-account'; import ModalEditAccount from './_components/accounts/modal-edit-account'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useUserList, useUserList,
useUpdateUserById, useUpdateUserById,
TUsersListItem, TUsersListItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/accounts')({
const [showModalEditAccount, setShowModalEditAccount] = useState(false); component: AccountsPage,
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null); })
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null); function AccountsPage() {
const [showModalEditAccount, setShowModalEditAccount] = useState(false)
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -40,31 +43,31 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false)
const { data: usersData, isLoading } = useUserList({ const { data: usersData, isLoading } = useUserList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const updateUser = useUpdateUserById(); const updateUser = useUpdateUserById()
const users: TUsersListItem[] = usersData?.data ?? []; const users: TUsersListItem[] = usersData?.data ?? []
const totalItems = usersData?.meta?.total ?? users.length; const totalItems = usersData?.meta?.total ?? users.length
const handleEditAccount = async () => { const handleEditAccount = async () => {
if (selectedUser && pendingFormData.current) { if (selectedUser && pendingFormData.current) {
await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current }); await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current })
} }
}; }
const columns: ColumnDef<TUsersListItem>[] = [ const columns: ColumnDef<TUsersListItem>[] = [
{ {
@@ -118,9 +121,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedUser(row.original); setSelectedUser(row.original)
setShowModalEditAccount(true); setShowModalEditAccount(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -128,7 +131,7 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: users, data: users,
@@ -144,7 +147,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -200,10 +203,8 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined} initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,12 +1,13 @@
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable } from '@imphnen-frontend-service/ui/organisms'; import { DataTable } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -14,27 +15,31 @@ import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalAddEvent from './_components/modal-add-event'; import ModalAddEvent from './_components/cms-events/modal-add-event'
import ModalUpdateEvent from './_components/modal-update-event'; import ModalUpdateEvent from './_components/cms-events/modal-update-event'
import ModalDeleteEvent from './_components/modal-delete-event'; import ModalDeleteEvent from './_components/cms-events/modal-delete-event'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useEventList, useEventList,
useCreateEvent, useCreateEvent,
useUpdateEvent, useUpdateEvent,
useDeleteEvent, useDeleteEvent,
TEventsListItem, TEventsListItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import React from 'react'; import React from 'react'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/cms-events')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: CmsEventsPage,
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<TEventsListItem | null>(null); function CmsEventsPage() {
const [search, setSearch] = useState(''); const [showModalAddItem, setShowModalAddItem] = useState(false)
const pendingFormData = useRef<any>(null); const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<TEventsListItem | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const { data: eventsData, isLoading } = useEventList({ const { data: eventsData, isLoading } = useEventList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const createEvent = useCreateEvent(); const createEvent = useCreateEvent()
const updateEvent = useUpdateEvent(); const updateEvent = useUpdateEvent()
const deleteEvent = useDeleteEvent(); const deleteEvent = useDeleteEvent()
const events: TEventsListItem[] = eventsData?.data ?? []; const events: TEventsListItem[] = eventsData?.data ?? []
const totalItems = eventsData?.meta?.total ?? events.length; const totalItems = eventsData?.meta?.total ?? events.length
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
await createEvent.mutateAsync(pendingFormData.current); await createEvent.mutateAsync(pendingFormData.current)
} }
return true; return true
}; }
const handleUpdate = async (): Promise<boolean> => { const handleUpdate = async (): Promise<boolean> => {
if (selectedEvent && pendingFormData.current) { if (selectedEvent && pendingFormData.current) {
await updateEvent.mutateAsync({ id: selectedEvent.id, data: pendingFormData.current }); await updateEvent.mutateAsync({ id: selectedEvent.id, data: pendingFormData.current })
} }
return true; return true
}; }
const handleDelete = async (): Promise<boolean> => { const handleDelete = async (): Promise<boolean> => {
if (selectedEvent) { if (selectedEvent) {
await deleteEvent.mutateAsync(selectedEvent.id); await deleteEvent.mutateAsync(selectedEvent.id)
} }
return true; return true
}; }
const columns: ColumnDef<TEventsListItem>[] = [ const columns: ColumnDef<TEventsListItem>[] = [
{ {
@@ -157,9 +162,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedEvent(row.original); setSelectedEvent(row.original)
setShowModalUpdateItem(true); setShowModalUpdateItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -169,9 +174,9 @@ export const Components: FC = (): ReactElement => {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedEvent(row.original); setSelectedEvent(row.original)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -180,7 +185,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: events, data: events,
@@ -196,7 +201,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -252,7 +257,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAdd={handleAdd} handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalUpdateEvent <ModalUpdateEvent
isOpen={showModalUpdateItem} isOpen={showModalUpdateItem}
@@ -271,7 +276,7 @@ export const Components: FC = (): ReactElement => {
end_date: selectedEvent.end_date, end_date: selectedEvent.end_date,
is_online: selectedEvent.is_online, is_online: selectedEvent.is_online,
} : undefined} } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeleteEvent <ModalDeleteEvent
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
@@ -282,7 +287,5 @@ export const Components: FC = (): ReactElement => {
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,12 +1,13 @@
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable } from '@imphnen-frontend-service/ui/organisms'; import { DataTable } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -14,27 +15,31 @@ import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalAddTestimonial from './_components/modal-add-testimonial'; import ModalAddTestimonial from './_components/cms-testimonials/modal-add-testimonial'
import ModalUpdateTestimonial from './_components/modal-update-testimonial'; import ModalUpdateTestimonial from './_components/cms-testimonials/modal-update-testimonial'
import ModalDeleteTestimonial from './_components/modal-delete-testimonial'; import ModalDeleteTestimonial from './_components/cms-testimonials/modal-delete-testimonial'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useTestimonialList, useTestimonialList,
useCreateTestimonial, useCreateTestimonial,
useUpdateTestimonial, useUpdateTestimonial,
useDeleteTestimonial, useDeleteTestimonial,
TTestimonialsListItem, TTestimonialsListItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import React from 'react'; import React from 'react'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/cms-testimonials')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: CmsTestimonialsPage,
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedTestimonial, setSelectedTestimonial] = useState<TTestimonialsListItem | null>(null); function CmsTestimonialsPage() {
const [search, setSearch] = useState(''); const [showModalAddItem, setShowModalAddItem] = useState(false)
const pendingFormData = useRef<any>(null); const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedTestimonial, setSelectedTestimonial] = useState<TTestimonialsListItem | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const { data: testimonialsData, isLoading } = useTestimonialList({ const { data: testimonialsData, isLoading } = useTestimonialList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const createTestimonial = useCreateTestimonial(); const createTestimonial = useCreateTestimonial()
const updateTestimonial = useUpdateTestimonial(); const updateTestimonial = useUpdateTestimonial()
const deleteTestimonial = useDeleteTestimonial(); const deleteTestimonial = useDeleteTestimonial()
const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []; const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []
const totalItems = testimonialsData?.meta?.total ?? testimonials.length; const totalItems = testimonialsData?.meta?.total ?? testimonials.length
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
await createTestimonial.mutateAsync(pendingFormData.current); await createTestimonial.mutateAsync(pendingFormData.current)
} }
return true; return true
}; }
const handleUpdate = async (): Promise<boolean> => { const handleUpdate = async (): Promise<boolean> => {
if (selectedTestimonial && pendingFormData.current) { if (selectedTestimonial && pendingFormData.current) {
await updateTestimonial.mutateAsync({ id: selectedTestimonial.id, data: pendingFormData.current }); await updateTestimonial.mutateAsync({ id: selectedTestimonial.id, data: pendingFormData.current })
} }
return true; return true
}; }
const handleDelete = async (): Promise<boolean> => { const handleDelete = async (): Promise<boolean> => {
if (selectedTestimonial) { if (selectedTestimonial) {
await deleteTestimonial.mutateAsync(selectedTestimonial.id); await deleteTestimonial.mutateAsync(selectedTestimonial.id)
} }
return true; return true
}; }
const columns: ColumnDef<TTestimonialsListItem>[] = [ const columns: ColumnDef<TTestimonialsListItem>[] = [
{ {
@@ -119,8 +124,8 @@ export const Components: FC = (): ReactElement => {
header: 'Content', header: 'Content',
accessorKey: 'content', accessorKey: 'content',
cell: ({ row }) => { cell: ({ row }) => {
const content = row.original.content; const content = row.original.content
return content.length > 80 ? `${content.substring(0, 80)}...` : content; return content.length > 80 ? `${content.substring(0, 80)}...` : content
}, },
}, },
{ {
@@ -141,9 +146,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedTestimonial(row.original); setSelectedTestimonial(row.original)
setShowModalUpdateItem(true); setShowModalUpdateItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -153,9 +158,9 @@ export const Components: FC = (): ReactElement => {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedTestimonial(row.original); setSelectedTestimonial(row.original)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -164,7 +169,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: testimonials, data: testimonials,
@@ -180,7 +185,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -236,7 +241,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAdd={handleAdd} handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalUpdateTestimonial <ModalUpdateTestimonial
isOpen={showModalUpdateItem} isOpen={showModalUpdateItem}
@@ -249,7 +254,7 @@ export const Components: FC = (): ReactElement => {
role: selectedTestimonial.role, role: selectedTestimonial.role,
content: selectedTestimonial.content, content: selectedTestimonial.content,
} : undefined} } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeleteTestimonial <ModalDeleteTestimonial
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
@@ -260,7 +265,5 @@ export const Components: FC = (): ReactElement => {
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,20 +1,25 @@
import { Button } from "@imphnen-frontend-service/ui/atoms"; import { createFileRoute } from '@tanstack/react-router'
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"; import { Button } from '@imphnen-frontend-service/ui/atoms'
import { For } from "@imphnen-frontend-service/utils"; import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
import { ReactElement } from "react"; import { For } from '@imphnen-frontend-service/utils'
import { UserGrowthChart } from "./_components/chart/user-growth"; import { ReactElement } from 'react'
import { SessionStatusChart } from "./_components/chart/session-status"; import { UserGrowthChart } from './_components/dashboard-dimentorin/chart/user-growth'
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service"; import { SessionStatusChart } from './_components/dashboard-dimentorin/chart/session-status'
import { useMentorList, useUserList, useMySessions } from '@imphnen-frontend-service/service'
export default function Components(): ReactElement { export const Route = createFileRoute('/_authenticated/dashboard-dimentorin')({
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' }); component: DashboardDimentorinPage,
const { data: userData } = useUserList({ per_page: 1 }); })
const { data: sessionsData } = useMySessions();
const totalMentors = mentorData?.meta?.total ?? 0; function DashboardDimentorinPage(): ReactElement {
const totalUsers = userData?.meta?.total ?? 0; const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' })
const totalSessions = sessionsData?.total ?? 0; const { data: userData } = useUserList({ per_page: 1 })
const topMentors = mentorData?.data ?? []; const { data: sessionsData } = useMySessions()
const totalMentors = mentorData?.meta?.total ?? 0
const totalUsers = userData?.meta?.total ?? 0
const totalSessions = sessionsData?.total ?? 0
const topMentors = mentorData?.data ?? []
const overviewStats = [ const overviewStats = [
{ label: 'Total Users', value: totalUsers }, { label: 'Total Users', value: totalUsers },
@@ -22,7 +27,7 @@ export default function Components(): ReactElement {
{ label: 'Total Sessions', value: totalSessions }, { label: 'Total Sessions', value: totalSessions },
{ label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length }, { label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length },
{ label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 }, { label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 },
]; ]
return ( return (
<BackofficeWrapper title="Dimentorin.dev"> <BackofficeWrapper title="Dimentorin.dev">
@@ -113,20 +118,20 @@ export default function Components(): ReactElement {
<tbody> <tbody>
{(() => { {(() => {
const sessions = sessionsData?.sessions ?? []; const sessions = sessionsData?.sessions ?? []
const topicCount: Record<string, number> = {}; const topicCount: Record<string, number> = {}
sessions.forEach((s) => { sessions.forEach((s) => {
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1; topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1
}); })
const topTopics = Object.entries(topicCount) const topTopics = Object.entries(topicCount)
.sort(([, a], [, b]) => b - a) .sort(([, a], [, b]) => b - a)
.slice(0, 5); .slice(0, 5)
if (topTopics.length === 0) { if (topTopics.length === 0) {
return ( return (
<tr> <tr>
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td> <td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
</tr> </tr>
); )
} }
return topTopics.map(([topic, count], index) => ( return topTopics.map(([topic, count], index) => (
<tr key={topic} className="shadow rounded-lg"> <tr key={topic} className="shadow rounded-lg">
@@ -134,7 +139,7 @@ export default function Components(): ReactElement {
<td className="py-4 px-5">{topic}</td> <td className="py-4 px-5">{topic}</td>
<td className="py-4 px-5">{count}</td> <td className="py-4 px-5">{count}</td>
</tr> </tr>
)); ))
})()} })()}
</tbody> </tbody>
</table> </table>
@@ -1,16 +1,17 @@
import { createFileRoute } from '@tanstack/react-router'
import { import {
PlusOutlined, PlusOutlined,
ReloadOutlined, ReloadOutlined,
UsergroupAddOutlined, UsergroupAddOutlined,
UsergroupDeleteOutlined, UsergroupDeleteOutlined,
UserSwitchOutlined, UserSwitchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import ModalAddItem from './_components/modal-add-item'; import ModalAddItem from './_components/dashboard/modal-add-item'
import ModalEditItem from './_components/modal-edit-item'; import ModalEditItem from './_components/dashboard/modal-edit-item'
import ModalDeleteItem from './_components/modal-delete-item'; import ModalDeleteItem from './_components/dashboard/modal-delete-item'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useUserList, useUserList,
useGachaItemList, useGachaItemList,
@@ -18,14 +19,18 @@ import {
useUpdateGachaItem, useUpdateGachaItem,
useDeleteGachaItem, useDeleteGachaItem,
TGachaItemDto, TGachaItemDto,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/dashboard')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: DashboardPage,
const [showModalEditItem, setShowModalEditItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null); function DashboardPage() {
const pendingFormData = useRef<any>(null); const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalEditItem, setShowModalEditItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null)
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -36,20 +41,20 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const { data: usersData } = useUserList({ per_page: 1 }); const { data: usersData } = useUserList({ per_page: 1 })
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 }); const { data: gachaItemsData } = useGachaItemList({ per_page: 9 })
const createItem = useCreateGachaItem(); const createItem = useCreateGachaItem()
const updateItem = useUpdateGachaItem(); const updateItem = useUpdateGachaItem()
const deleteItem = useDeleteGachaItem(); const deleteItem = useDeleteGachaItem()
const totalUsers = usersData?.meta?.total ?? 0; const totalUsers = usersData?.meta?.total ?? 0
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? []; const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? []
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current; const { itemName, quantity } = pendingFormData.current
await createItem.mutateAsync({ await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'), item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName, name: itemName,
@@ -61,28 +66,28 @@ export const Components: FC = (): ReactElement => {
weight: 1, weight: 1,
stock: quantity ?? 1, stock: quantity ?? 1,
is_limited: false, is_limited: false,
}); })
} }
return true; return true
}; }
const handleEdit = async (): Promise<boolean> => { const handleEdit = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) { if (selectedItem && pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current; const { itemName, quantity } = pendingFormData.current
await updateItem.mutateAsync({ await updateItem.mutateAsync({
id: selectedItem.id, id: selectedItem.id,
data: { name: itemName, stock: quantity }, data: { name: itemName, stock: quantity },
}); })
} }
return true; return true
}; }
const handleDelete = async (): Promise<boolean> => { const handleDelete = async (): Promise<boolean> => {
if (selectedItem) { if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id); await deleteItem.mutateAsync(selectedItem.id)
} }
return true; return true
}; }
return ( return (
<Fragment> <Fragment>
@@ -181,8 +186,8 @@ export const Components: FC = (): ReactElement => {
size="sm" size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500" className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => { onClick={() => {
setSelectedItem(item); setSelectedItem(item)
setShowModalEditItem(true); setShowModalEditItem(true)
}} }}
> >
Edit Edit
@@ -192,8 +197,8 @@ export const Components: FC = (): ReactElement => {
size="sm" size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700" className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => { onClick={() => {
setSelectedItem(item); setSelectedItem(item)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
> >
Delete Delete
@@ -224,7 +229,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAddItem={handleAdd} handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalEditItem <ModalEditItem
@@ -236,16 +241,14 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
handleEditItem={handleEdit} handleEditItem={handleEdit}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined} initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeleteItem <ModalDeleteItem
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)} onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={async () => { await handleDelete(); return true; }} handleDeleteItem={async () => { await handleDelete(); return true }}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,10 +1,11 @@
import { SearchOutlined } from "@ant-design/icons"; import { createFileRoute } from '@tanstack/react-router'
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms"; import { SearchOutlined } from '@ant-design/icons'
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms"; import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { cn, For } from "@imphnen-frontend-service/utils"; import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"; import { cn, For } from '@imphnen-frontend-service/utils'
import { ReactElement, useState } from "react" import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service"; import { ReactElement, useState } from 'react'
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
const TABS = { const TABS = {
MENTORING: 'Mentoring', MENTORING: 'Mentoring',
@@ -12,30 +13,34 @@ const TABS = {
} as const } as const
type Tabs = typeof TABS[keyof typeof TABS] type Tabs = typeof TABS[keyof typeof TABS]
export default function Components(): ReactElement { export const Route = createFileRoute('/_authenticated/feedback-review-dimentorin')({
component: FeedbackReviewDimentorinPage,
})
function FeedbackReviewDimentorinPage(): ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING) const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const { data: sessionsData, isLoading } = useMySessions( const { data: sessionsData, isLoading } = useMySessions(
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
); )
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
? (sessionsData?.sessions ?? []) ? (sessionsData?.sessions ?? [])
: []; : []
const totalItems = activeTab === TABS.MENTORING const totalItems = activeTab === TABS.MENTORING
? (sessionsData?.total ?? sessions.length) ? (sessionsData?.total ?? sessions.length)
: 0; : 0
const columns: ColumnDef<TSessionListItem>[] = [ const columns: ColumnDef<TSessionListItem>[] = [
{ {
id: 'select', id: 'select',
meta: { cellClassName: cn("w-20") }, meta: { cellClassName: cn('w-20') },
header: ({ table }) => ( header: ({ table }) => (
<input <input
type="checkbox" type="checkbox"
@@ -76,17 +81,17 @@ export default function Components(): ReactElement {
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const hasRating = !!row.original.rating; const hasRating = !!row.original.rating
return ( return (
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}> <div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
{hasRating ? 'Done' : 'To Do'} {hasRating ? 'Done' : 'To Do'}
</div> </div>
); )
}, },
}, },
{ {
header: 'Action', header: 'Action',
meta: { cellClassName: cn("w-72") }, meta: { cellClassName: cn('w-72') },
cell: () => ( cell: () => (
<Button <Button
variant="primary" variant="primary"
@@ -116,7 +121,7 @@ export default function Components(): ReactElement {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<BackofficeWrapper title="Dimentorin.dev"> <BackofficeWrapper title="Dimentorin.dev">
@@ -128,10 +133,10 @@ export default function Components(): ReactElement {
<Button <Button
key={tab} key={tab}
variant="text" variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")} className={cn('px-3 py-2 capitalize', activeTab === tab && 'bg-white')}
onClick={() => { onClick={() => {
setActiveTab(tab); setActiveTab(tab)
setPagination((p) => ({ ...p, pageIndex: 0 })); setPagination((p) => ({ ...p, pageIndex: 0 }))
}} }}
> >
{tab} {tab}
@@ -175,5 +180,5 @@ export default function Components(): ReactElement {
)} )}
</section> </section>
</BackofficeWrapper> </BackofficeWrapper>
); )
} }
@@ -1,15 +1,14 @@
import * as React from 'react'; import { createFileRoute } from '@tanstack/react-router'
import * as React from 'react'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable } from '@imphnen-frontend-service/ui/organisms'; import { DataTable } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -17,26 +16,30 @@ import {
PaginationState, PaginationState,
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalAddItem from './_components/modal-add-item'; import ModalAddItem from './_components/gacha-roll/modal-add-item'
import ModalUpdateItem from './_components/modal-update-item'; import ModalUpdateItem from './_components/gacha-roll/modal-update-item'
import ModalDeleteItem from './_components/modal-delete-item'; import ModalDeleteItem from './_components/gacha-roll/modal-delete-item'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useGachaItemList, useGachaItemList,
useCreateGachaItem, useCreateGachaItem,
useUpdateGachaItem, useUpdateGachaItem,
useDeleteGachaItem, useDeleteGachaItem,
TGachaItemDto, TGachaItemDto,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/gacha-roll')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: GachaRollPage,
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null); function GachaRollPage() {
const [search, setSearch] = useState(''); const [showModalAddItem, setShowModalAddItem] = useState(false)
const pendingFormData = useRef<any>(null); const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -47,30 +50,30 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const { data: itemsData, isLoading } = useGachaItemList({ const { data: itemsData, isLoading } = useGachaItemList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const createItem = useCreateGachaItem(); const createItem = useCreateGachaItem()
const updateItem = useUpdateGachaItem(); const updateItem = useUpdateGachaItem()
const deleteItem = useDeleteGachaItem(); const deleteItem = useDeleteGachaItem()
const items: TGachaItemDto[] = itemsData?.data ?? []; const items: TGachaItemDto[] = itemsData?.data ?? []
const totalItems = itemsData?.meta?.total ?? items.length; const totalItems = itemsData?.meta?.total ?? items.length
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current; const { itemName, quantity, chanceRate } = pendingFormData.current
await createItem.mutateAsync({ await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'), item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName, name: itemName,
@@ -82,14 +85,14 @@ export const Components: FC = (): ReactElement => {
weight: chanceRate ?? 1, weight: chanceRate ?? 1,
stock: quantity ?? 1, stock: quantity ?? 1,
is_limited: false, is_limited: false,
}); })
} }
return true; return true
}; }
const handleUpdate = async (): Promise<boolean> => { const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) { if (selectedItem && pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current; const { itemName, quantity, chanceRate } = pendingFormData.current
await updateItem.mutateAsync({ await updateItem.mutateAsync({
id: selectedItem.id, id: selectedItem.id,
data: { data: {
@@ -97,17 +100,17 @@ export const Components: FC = (): ReactElement => {
weight: chanceRate, weight: chanceRate,
stock: quantity, stock: quantity,
}, },
}); })
} }
return true; return true
}; }
const handleDelete = async () => { const handleDelete = async () => {
if (selectedItem) { if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id); await deleteItem.mutateAsync(selectedItem.id)
} }
setShowModalDeleteItem(false); setShowModalDeleteItem(false)
}; }
const columns: ColumnDef<TGachaItemDto>[] = [ const columns: ColumnDef<TGachaItemDto>[] = [
{ {
@@ -145,9 +148,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedItem(row.original); setSelectedItem(row.original)
setShowModalUpdateItem(true); setShowModalUpdateItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -157,9 +160,9 @@ export const Components: FC = (): ReactElement => {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedItem(row.original); setSelectedItem(row.original)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -168,7 +171,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: items, data: items,
@@ -184,7 +187,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -235,7 +238,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAddItem={handleAdd} handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalUpdateItem <ModalUpdateItem
currentStep={currentStep} currentStep={currentStep}
@@ -246,7 +249,7 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
handleUpdateItem={handleUpdate} handleUpdateItem={handleUpdate}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined} initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeleteItem <ModalDeleteItem
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
@@ -254,7 +257,5 @@ export const Components: FC = (): ReactElement => {
handleDeleteItem={handleDelete} handleDeleteItem={handleDelete}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,31 +1,36 @@
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; import { createFileRoute } from '@tanstack/react-router'
import { FC, ReactElement } from 'react'; import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
import { useQuery } from '@tanstack/react-query'; import { FC, ReactElement } from 'react'
import { useQuery } from '@tanstack/react-query'
import { import {
getAdminUsers, getAdminUsers,
getAdminTeams, getAdminTeams,
getAdminSubmissions, getAdminSubmissions,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
export const HackathonDashboardPage: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/hackathon-dashboard')({
component: HackathonDashboardPage,
})
function HackathonDashboardPage() {
const { data: usersData } = useQuery({ const { data: usersData } = useQuery({
queryKey: ['admin-users-count'], queryKey: ['admin-users-count'],
queryFn: () => getAdminUsers({ page: 1, per_page: 1 }), queryFn: () => getAdminUsers({ page: 1, per_page: 1 }),
}); })
const { data: teamsData } = useQuery({ const { data: teamsData } = useQuery({
queryKey: ['admin-teams-count'], queryKey: ['admin-teams-count'],
queryFn: () => getAdminTeams({ page: 1, per_page: 1 }), queryFn: () => getAdminTeams({ page: 1, per_page: 1 }),
}); })
const { data: submissionsData } = useQuery({ const { data: submissionsData } = useQuery({
queryKey: ['admin-submissions-count'], queryKey: ['admin-submissions-count'],
queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }), queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }),
}); })
const totalParticipants = usersData?.meta?.total_data ?? '??'; const totalParticipants = usersData?.meta?.total_data ?? '??'
const totalTeams = teamsData?.meta?.total_data ?? '??'; const totalTeams = teamsData?.meta?.total_data ?? '??'
const totalSubmissions = submissionsData?.meta?.total_data ?? '??'; const totalSubmissions = submissionsData?.meta?.total_data ?? '??'
return ( return (
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025"> <BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
@@ -52,7 +57,5 @@ export const HackathonDashboardPage: FC = (): ReactElement => {
</div> </div>
</section> </section>
</BackofficeWrapper> </BackofficeWrapper>
); )
}; }
export default HackathonDashboardPage;
@@ -1,3 +1,4 @@
import { createFileRoute } from '@tanstack/react-router'
import { import {
FC, FC,
ReactElement, ReactElement,
@@ -5,44 +6,52 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useCallback, useCallback,
} from 'react'; } from 'react'
import SubmissionModal from './_components/submission-modal'; import SubmissionModal from './_components/hackathon-submissions/submission-modal'
import { import {
BackofficeWrapper, BackofficeWrapper,
DataTable, DataTable,
} from '@imphnen-frontend-service/ui/organisms'; } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table'
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms'
import { cn } from '@imphnen-frontend-service/utils'; import { cn } from '@imphnen-frontend-service/utils'
import { import {
SearchOutlined, SearchOutlined,
FilterOutlined, FilterOutlined,
LoadingOutlined, LoadingOutlined,
EyeOutlined, EyeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'
import { import {
getAdminSubmissions, getAdminSubmissions,
TAdminSubmissionItem, TAdminSubmissionItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import { useSearchParams } from 'react-router-dom'; import { useNavigate } from '@tanstack/react-router'
type SubmissionType = TAdminSubmissionItem; type SubmissionType = TAdminSubmissionItem
export const HackathonSubmissionsPage: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/hackathon-submissions')({
const [searchParams, setSearchParams] = useSearchParams(); component: HackathonSubmissionsPage,
const currentPage = Math.max( validateSearch: (search: Record<string, unknown>) => ({
1, page: Number(search.page) || 1,
parseInt(searchParams.get('page') || '1', 10) search: (search.search as string) || '',
); per_page: Number(search.per_page) || 10,
const searchQuery = searchParams.get('search') || ''; status: (search.status as string) || 'all',
const perPage = parseInt(searchParams.get('per_page') || '10', 10); }),
const statusFilter = searchParams.get('status') || 'all'; })
const [showSubmissionModal, setShowSubmissionModal] = useState(false); function HackathonSubmissionsPage() {
const searchParams = Route.useSearch()
const navigate = useNavigate()
const currentPage = Math.max(1, searchParams.page)
const searchQuery = searchParams.search || ''
const perPage = searchParams.per_page || 10
const statusFilter = searchParams.status || 'all'
const [showSubmissionModal, setShowSubmissionModal] = useState(false)
const [selectedSubmission, setSelectedSubmission] = const [selectedSubmission, setSelectedSubmission] =
useState<SubmissionType | null>(null); useState<SubmissionType | null>(null)
const [globalFilter, setGlobalFilter] = useState(searchQuery); const [globalFilter, setGlobalFilter] = useState(searchQuery)
const { const {
data: submissionsResponse, data: submissionsResponse,
@@ -65,82 +74,86 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
}), }),
staleTime: 30000, staleTime: 30000,
gcTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000,
}); })
const totalData = submissionsResponse?.meta?.total_data || 0; const totalData = submissionsResponse?.meta?.total_data || 0
const totalPages = submissionsResponse?.meta?.total_page || 1; const totalPages = submissionsResponse?.meta?.total_page || 1
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', newPage.toString()); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: newPage,
if (searchQuery) params.set('search', searchQuery); per_page: perPage !== 10 ? perPage : undefined,
if (statusFilter !== 'all') params.set('status', statusFilter); search: searchQuery || undefined,
setSearchParams(params); status: statusFilter !== 'all' ? statusFilter : undefined,
window.scrollTo({ top: 0, behavior: 'smooth' }); } as any,
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}, },
[setSearchParams, perPage, searchQuery, statusFilter] [navigate, perPage, searchQuery, statusFilter]
); )
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); navigate({ search: { page: totalPages } as any })
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, navigate, isLoading])
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery)
}, [searchQuery]); }, [searchQuery])
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: 1,
if (globalFilter.trim()) { per_page: perPage !== 10 ? perPage : undefined,
params.set('search', globalFilter.trim()); search: globalFilter.trim() || undefined,
} status: statusFilter !== 'all' ? statusFilter : undefined,
if (statusFilter !== 'all') params.set('status', statusFilter); } as any,
setSearchParams(params); })
}, [globalFilter, setSearchParams, perPage, statusFilter]); }, [globalFilter, navigate, perPage, statusFilter])
const handleSearchKeyPress = useCallback( const handleSearchKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleSearch(); handleSearch()
} }
}, },
[handleSearch] [handleSearch]
); )
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
params.set('per_page', newPerPage.toString()); page: 1,
if (searchQuery) params.set('search', searchQuery); per_page: newPerPage,
if (statusFilter !== 'all') params.set('status', statusFilter); search: searchQuery || undefined,
setSearchParams(params); status: statusFilter !== 'all' ? statusFilter : undefined,
} as any,
})
}, },
[setSearchParams, searchQuery, statusFilter] [navigate, searchQuery, statusFilter]
); )
const handleShowSubmissionModal = useCallback( const handleShowSubmissionModal = useCallback(
(submission: SubmissionType) => { (submission: SubmissionType) => {
setSelectedSubmission(submission); setSelectedSubmission(submission)
setShowSubmissionModal(true); setShowSubmissionModal(true)
}, },
[] []
); )
const handleCloseSubmissionModal = useCallback(() => { const handleCloseSubmissionModal = useCallback(() => {
setShowSubmissionModal(false); setShowSubmissionModal(false)
setSelectedSubmission(null); setSelectedSubmission(null)
}, []); }, [])
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return submissionsResponse?.data || []; return submissionsResponse?.data || []
}, [submissionsResponse]); }, [submissionsResponse])
const columns: ColumnDef<SubmissionType>[] = useMemo( const columns: ColumnDef<SubmissionType>[] = useMemo(
() => [ () => [
@@ -168,7 +181,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
accessorKey: 'status', accessorKey: 'status',
header: 'Status', header: 'Status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
return ( return (
<span <span
className={cn( className={cn(
@@ -182,7 +195,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
> >
{status.charAt(0).toUpperCase() + status.slice(1)} {status.charAt(0).toUpperCase() + status.slice(1)}
</span> </span>
); )
}, },
enableSorting: true, enableSorting: true,
}, },
@@ -220,7 +233,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
}, },
], ],
[handleShowSubmissionModal] [handleShowSubmissionModal]
); )
return ( return (
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025"> <BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
@@ -308,7 +321,5 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
/> />
)} )}
</BackofficeWrapper> </BackofficeWrapper>
); )
}; }
export default HackathonSubmissionsPage;
@@ -1,3 +1,4 @@
import { createFileRoute } from '@tanstack/react-router'
import { import {
FC, FC,
ReactElement, ReactElement,
@@ -5,16 +6,16 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useCallback, useCallback,
} from 'react'; } from 'react'
import ModalTeamDetail from './_components/modal-team-detail-new'; import ModalTeamDetail from './_components/hackathon-teams/modal-team-detail-new'
import { CityFilterSelect } from '../../../components/city-filter-select'; import { CityFilterSelect } from '../../components/city-filter-select'
import { import {
BackofficeWrapper, BackofficeWrapper,
DataTable, DataTable,
} from '@imphnen-frontend-service/ui/organisms'; } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table'
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms'
import { cn } from '@imphnen-frontend-service/utils'; import { cn } from '@imphnen-frontend-service/utils'
import { import {
EditOutlined, EditOutlined,
TeamOutlined, TeamOutlined,
@@ -22,32 +23,39 @@ import {
FilterOutlined, FilterOutlined,
PlusOutlined, PlusOutlined,
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'
import { import {
getAdminTeams, getAdminTeams,
TAdminTeamItem, TAdminTeamItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import { useSearchParams } from 'react-router-dom'; import { useNavigate } from '@tanstack/react-router'
type TeamType = TAdminTeamItem; type TeamType = TAdminTeamItem
export const HackathonTeamsPage: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/hackathon-teams')({
const [searchParams, setSearchParams] = useSearchParams(); component: HackathonTeamsPage,
const currentPage = Math.max( validateSearch: (search: Record<string, unknown>) => ({
1, page: Number(search.page) || 1,
parseInt(searchParams.get('page') || '1', 10) search: (search.search as string) || '',
); per_page: Number(search.per_page) || 10,
const searchQuery = searchParams.get('search') || ''; }),
const perPage = parseInt(searchParams.get('per_page') || '10', 10); })
const [showDetailModal, setShowDetailModal] = useState(false);
const [showNewTeamModal, setShowNewTeamModal] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
useState<TeamType | null>(null);
const [globalFilter, setGlobalFilter] = useState(searchQuery);
const [visibilityFilter, setVisibilityFilter] = useState('all'); function HackathonTeamsPage() {
const [cityFilter, setCityFilter] = useState('all'); const searchParams = Route.useSearch()
const navigate = useNavigate()
const currentPage = Math.max(1, searchParams.page)
const searchQuery = searchParams.search || ''
const perPage = searchParams.per_page || 10
const [showDetailModal, setShowDetailModal] = useState(false)
const [showNewTeamModal, setShowNewTeamModal] = useState(false)
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null)
useState<TeamType | null>(null)
const [globalFilter, setGlobalFilter] = useState(searchQuery)
const [visibilityFilter, setVisibilityFilter] = useState('all')
const [cityFilter, setCityFilter] = useState('all')
const { const {
data: teamsResponse, data: teamsResponse,
@@ -70,84 +78,88 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
}), }),
staleTime: 30000, staleTime: 30000,
gcTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000,
}); })
const totalData = teamsResponse?.meta?.total_data || 0; const totalData = teamsResponse?.meta?.total_data || 0
const totalPages = teamsResponse?.meta?.total_page || 1; const totalPages = teamsResponse?.meta?.total_page || 1
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', newPage.toString()); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: newPage,
if (searchQuery) params.set('search', searchQuery); per_page: perPage !== 10 ? perPage : undefined,
setSearchParams(params); search: searchQuery || undefined,
window.scrollTo({ top: 0, behavior: 'smooth' }); } as any,
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}, },
[setSearchParams, perPage, searchQuery] [navigate, perPage, searchQuery]
); )
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); navigate({ search: { page: totalPages } as any })
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, navigate, isLoading])
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery)
}, [searchQuery]); }, [searchQuery])
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: 1,
if (globalFilter.trim()) { per_page: perPage !== 10 ? perPage : undefined,
params.set('search', globalFilter.trim()); search: globalFilter.trim() || undefined,
} } as any,
setSearchParams(params); })
}, [globalFilter, setSearchParams, perPage]); }, [globalFilter, navigate, perPage])
const handleSearchKeyPress = useCallback( const handleSearchKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleSearch(); handleSearch()
} }
}, },
[handleSearch] [handleSearch]
); )
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
params.set('per_page', newPerPage.toString()); page: 1,
if (searchQuery) params.set('search', searchQuery); per_page: newPerPage,
setSearchParams(params); search: searchQuery || undefined,
} as any,
})
}, },
[setSearchParams, searchQuery] [navigate, searchQuery]
); )
const handleShowDetailModal = useCallback((team: TeamType) => { const handleShowDetailModal = useCallback((team: TeamType) => {
setSelectedTeam(team); setSelectedTeam(team)
setShowDetailModal(true); setShowDetailModal(true)
}, []); }, [])
const handleCloseDetailModal = useCallback(() => { const handleCloseDetailModal = useCallback(() => {
setShowDetailModal(false); setShowDetailModal(false)
setSelectedTeam(null); setSelectedTeam(null)
}, []); }, [])
const handleShowNewTeamModal = useCallback(() => { const handleShowNewTeamModal = useCallback(() => {
setShowNewTeamModal(true); setShowNewTeamModal(true)
}, []); }, [])
const handleCloseNewTeamModal = useCallback(() => { const handleCloseNewTeamModal = useCallback(() => {
setShowNewTeamModal(false); setShowNewTeamModal(false)
}, []); }, [])
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return teamsResponse?.data || []; return teamsResponse?.data || []
}, [teamsResponse]); }, [teamsResponse])
const columns: ColumnDef<TeamType>[] = useMemo( const columns: ColumnDef<TeamType>[] = useMemo(
() => [ () => [
@@ -155,7 +167,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
accessorKey: 'name', accessorKey: 'name',
header: 'Team', header: 'Team',
cell: ({ row }) => { cell: ({ row }) => {
const team = row.original; const team = row.original
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden"> <div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden">
@@ -178,7 +190,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
</p> </p>
</div> </div>
</div> </div>
); )
}, },
enableSorting: true, enableSorting: true,
}, },
@@ -194,7 +206,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
accessorKey: 'visibility', accessorKey: 'visibility',
header: 'Visibility', header: 'Visibility',
cell: ({ row }) => { cell: ({ row }) => {
const isPublic = row.original.visibility === 'public'; const isPublic = row.original.visibility === 'public'
return ( return (
<span <span
className={cn( className={cn(
@@ -206,7 +218,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
> >
{isPublic ? 'Public' : 'Private'} {isPublic ? 'Public' : 'Private'}
</span> </span>
); )
}, },
enableSorting: true, enableSorting: true,
}, },
@@ -256,7 +268,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
}, },
], ],
[handleShowDetailModal] [handleShowDetailModal]
); )
return ( return (
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025"> <BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
@@ -347,9 +359,9 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={() => { onClick={() => {
setVisibilityFilter('all'); setVisibilityFilter('all')
setCityFilter('all'); setCityFilter('all')
setGlobalFilter(''); setGlobalFilter('')
}} }}
className="text-sm text-neutral-600" className="text-sm text-neutral-600"
> >
@@ -401,7 +413,5 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
team={null} team={null}
/> />
</BackofficeWrapper> </BackofficeWrapper>
); )
}; }
export default HackathonTeamsPage;
@@ -1,3 +1,4 @@
import { createFileRoute } from '@tanstack/react-router'
import { import {
FC, FC,
ReactElement, ReactElement,
@@ -5,15 +6,15 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useCallback, useCallback,
} from 'react'; } from 'react'
import ModalUserDetail from './_components/modal-user-detail'; import ModalUserDetail from './_components/hackathon-users/modal-user-detail'
import { import {
BackofficeWrapper, BackofficeWrapper,
DataTable, DataTable,
} from '@imphnen-frontend-service/ui/organisms'; } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table'
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms'
import { cn } from '@imphnen-frontend-service/utils'; import { cn } from '@imphnen-frontend-service/utils'
import { import {
EditOutlined, EditOutlined,
UserOutlined, UserOutlined,
@@ -21,16 +22,16 @@ import {
FilterOutlined, FilterOutlined,
PlusOutlined, PlusOutlined,
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { CityFilterSelect } from '../../../components/city-filter-select'; import { CityFilterSelect } from '../../components/city-filter-select'
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'
import { import {
getAdminUsers, getAdminUsers,
TAdminUserItem, TAdminUserItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import { useSearchParams } from 'react-router-dom'; import { useSearch, useNavigate } from '@tanstack/react-router'
type UserType = TAdminUserItem; type UserType = TAdminUserItem
const skillsOptions = [ const skillsOptions = [
'Frontend Developer', 'Frontend Developer',
@@ -41,24 +42,31 @@ const skillsOptions = [
'Product Manager', 'Product Manager',
'Data Scientist', 'Data Scientist',
'Mobile Developer', 'Mobile Developer',
]; ]
export const HackathonUsersPage: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/hackathon-users')({
const [searchParams, setSearchParams] = useSearchParams(); component: HackathonUsersPage,
const currentPage = Math.max( validateSearch: (search: Record<string, unknown>) => ({
1, page: Number(search.page) || 1,
parseInt(searchParams.get('page') || '1', 10) search: (search.search as string) || '',
); per_page: Number(search.per_page) || 10,
const searchQuery = searchParams.get('search') || ''; }),
const perPage = parseInt(searchParams.get('per_page') || '10', 10); })
const [showDetailModal, setShowDetailModal] = useState(false);
const [showNewUserModal, setShowNewUserModal] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserType | null>(null);
const [globalFilter, setGlobalFilter] = useState(searchQuery);
const [statusFilter, setStatusFilter] = useState('all'); function HackathonUsersPage() {
const [cityFilter, setCityFilter] = useState('all'); const searchParams = Route.useSearch()
const [skillsFilter, setSkillsFilter] = useState<string[]>([]); const navigate = useNavigate()
const currentPage = Math.max(1, searchParams.page)
const searchQuery = searchParams.search || ''
const perPage = searchParams.per_page || 10
const [showDetailModal, setShowDetailModal] = useState(false)
const [showNewUserModal, setShowNewUserModal] = useState(false)
const [selectedUser, setSelectedUser] = useState<UserType | null>(null)
const [globalFilter, setGlobalFilter] = useState(searchQuery)
const [statusFilter, setStatusFilter] = useState('all')
const [cityFilter, setCityFilter] = useState('all')
const [skillsFilter, setSkillsFilter] = useState<string[]>([])
const { const {
data: usersResponse, data: usersResponse,
@@ -81,100 +89,104 @@ export const HackathonUsersPage: FC = (): ReactElement => {
}), }),
staleTime: 30000, staleTime: 30000,
gcTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000,
}); })
const totalData = usersResponse?.meta?.total_data || 0; const totalData = usersResponse?.meta?.total_data || 0
const totalPages = usersResponse?.meta?.total_page || 1; const totalPages = usersResponse?.meta?.total_page || 1
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', newPage.toString()); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: newPage,
if (searchQuery) params.set('search', searchQuery); per_page: perPage !== 10 ? perPage : undefined,
setSearchParams(params); search: searchQuery || undefined,
window.scrollTo({ top: 0, behavior: 'smooth' }); } as any,
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}, },
[setSearchParams, perPage, searchQuery] [navigate, perPage, searchQuery]
); )
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); navigate({ search: { page: totalPages } as any })
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, navigate, isLoading])
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery)
}, [searchQuery]); }, [searchQuery])
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
if (perPage !== 10) params.set('per_page', perPage.toString()); page: 1,
if (globalFilter.trim()) { per_page: perPage !== 10 ? perPage : undefined,
params.set('search', globalFilter.trim()); search: globalFilter.trim() || undefined,
} } as any,
setSearchParams(params); })
}, [globalFilter, setSearchParams, perPage]); }, [globalFilter, navigate, perPage])
const handleSearchKeyPress = useCallback( const handleSearchKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleSearch(); handleSearch()
} }
}, },
[handleSearch] [handleSearch]
); )
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); navigate({
params.set('page', '1'); search: {
params.set('per_page', newPerPage.toString()); page: 1,
if (searchQuery) params.set('search', searchQuery); per_page: newPerPage,
setSearchParams(params); search: searchQuery || undefined,
} as any,
})
}, },
[setSearchParams, searchQuery] [navigate, searchQuery]
); )
const handleShowDetailModal = useCallback((user: UserType) => { const handleShowDetailModal = useCallback((user: UserType) => {
setSelectedUser(user); setSelectedUser(user)
setShowDetailModal(true); setShowDetailModal(true)
}, []); }, [])
const handleCloseDetailModal = useCallback(() => { const handleCloseDetailModal = useCallback(() => {
setShowDetailModal(false); setShowDetailModal(false)
setSelectedUser(null); setSelectedUser(null)
}, []); }, [])
const handleShowNewUserModal = useCallback(() => { const handleShowNewUserModal = useCallback(() => {
setShowNewUserModal(true); setShowNewUserModal(true)
}, []); }, [])
const handleCloseNewUserModal = useCallback(() => { const handleCloseNewUserModal = useCallback(() => {
setShowNewUserModal(false); setShowNewUserModal(false)
}, []); }, [])
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
const usersData = usersResponse?.data || []; const usersData = usersResponse?.data || []
return usersData.filter((user: UserType) => { return usersData.filter((user: UserType) => {
if (statusFilter !== 'all') { if (statusFilter !== 'all') {
const isActive = statusFilter === 'active'; const isActive = statusFilter === 'active'
if (user.is_active !== isActive) return false; if (user.is_active !== isActive) return false
} }
if (skillsFilter.length > 0) { if (skillsFilter.length > 0) {
const userSkills = user.skills || []; const userSkills = user.skills || []
const hasMatchingSkill = skillsFilter.some((skill) => const hasMatchingSkill = skillsFilter.some((skill) =>
userSkills.includes(skill) userSkills.includes(skill)
); )
if (!hasMatchingSkill) return false; if (!hasMatchingSkill) return false
} }
return true; return true
}); })
}, [usersResponse, statusFilter, skillsFilter]); }, [usersResponse, statusFilter, skillsFilter])
const columns: ColumnDef<UserType>[] = useMemo( const columns: ColumnDef<UserType>[] = useMemo(
() => [ () => [
@@ -207,7 +219,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
accessorKey: 'skills', accessorKey: 'skills',
header: 'Skills', header: 'Skills',
cell: ({ row }) => { cell: ({ row }) => {
const skills = row.original.skills || []; const skills = row.original.skills || []
return ( return (
<div className="flex flex-wrap gap-1 max-w-xs"> <div className="flex flex-wrap gap-1 max-w-xs">
{skills.length > 0 ? ( {skills.length > 0 ? (
@@ -230,7 +242,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
<span className="text-neutral-400">-</span> <span className="text-neutral-400">-</span>
)} )}
</div> </div>
); )
}, },
enableSorting: false, enableSorting: false,
}, },
@@ -265,11 +277,11 @@ export const HackathonUsersPage: FC = (): ReactElement => {
), ),
enableSorting: true, enableSorting: true,
sortingFn: (rowA, rowB) => { sortingFn: (rowA, rowB) => {
const aActive = rowA.original.is_active; const aActive = rowA.original.is_active
const bActive = rowB.original.is_active; const bActive = rowB.original.is_active
if (aActive && !bActive) return -1; if (aActive && !bActive) return -1
if (!aActive && bActive) return 1; if (!aActive && bActive) return 1
return 0; return 0
}, },
}, },
{ {
@@ -310,7 +322,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
}, },
], ],
[handleShowDetailModal] [handleShowDetailModal]
); )
return ( return (
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025"> <BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
@@ -424,10 +436,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={() => { onClick={() => {
setStatusFilter('all'); setStatusFilter('all')
setCityFilter('all'); setCityFilter('all')
setSkillsFilter([]); setSkillsFilter([])
setGlobalFilter(''); setGlobalFilter('')
}} }}
className="text-sm text-neutral-600" className="text-sm text-neutral-600"
> >
@@ -479,7 +491,5 @@ export const HackathonUsersPage: FC = (): ReactElement => {
user={null} user={null}
/> />
</BackofficeWrapper> </BackofficeWrapper>
); )
}; }
export default HackathonUsersPage;
@@ -1,12 +1,13 @@
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable } from '@imphnen-frontend-service/ui/organisms'; import { DataTable } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -14,27 +15,31 @@ import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalAddPermission from './_components/modal-add-permission'; import ModalAddPermission from './_components/permissions/modal-add-permission'
import ModalUpdatePermission from './_components/modal-update-permission'; import ModalUpdatePermission from './_components/permissions/modal-update-permission'
import ModalDeletePermission from './_components/modal-delete-permission'; import ModalDeletePermission from './_components/permissions/modal-delete-permission'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
usePermissionList, usePermissionList,
useCreatePermission, useCreatePermission,
useUpdatePermission, useUpdatePermission,
useDeletePermission, useDeletePermission,
TPermissionItem, TPermissionItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import React from 'react'; import React from 'react'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/permissions')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: PermissionsPage,
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null); function PermissionsPage() {
const [search, setSearch] = useState(''); const [showModalAddItem, setShowModalAddItem] = useState(false)
const pendingFormData = useRef<any>(null); const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const { data: permissionsData, isLoading } = usePermissionList({ const { data: permissionsData, isLoading } = usePermissionList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const createPermission = useCreatePermission(); const createPermission = useCreatePermission()
const updatePermission = useUpdatePermission(); const updatePermission = useUpdatePermission()
const deletePermission = useDeletePermission(); const deletePermission = useDeletePermission()
const permissions: TPermissionItem[] = permissionsData?.data ?? []; const permissions: TPermissionItem[] = permissionsData?.data ?? []
const totalItems = permissionsData?.meta?.total ?? permissions.length; const totalItems = permissionsData?.meta?.total ?? permissions.length
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
await createPermission.mutateAsync(pendingFormData.current); await createPermission.mutateAsync(pendingFormData.current)
} }
return true; return true
}; }
const handleUpdate = async (): Promise<boolean> => { const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) { if (selectedItem && pendingFormData.current) {
await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current }); await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current })
} }
return true; return true
}; }
const handleDelete = async (): Promise<boolean> => { const handleDelete = async (): Promise<boolean> => {
if (selectedItem) { if (selectedItem) {
await deletePermission.mutateAsync(selectedItem.id); await deletePermission.mutateAsync(selectedItem.id)
} }
return true; return true
}; }
const columns: ColumnDef<TPermissionItem>[] = [ const columns: ColumnDef<TPermissionItem>[] = [
{ {
@@ -123,9 +128,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedItem(row.original); setSelectedItem(row.original)
setShowModalUpdateItem(true); setShowModalUpdateItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -135,9 +140,9 @@ export const Components: FC = (): ReactElement => {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedItem(row.original); setSelectedItem(row.original)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -146,7 +151,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: permissions, data: permissions,
@@ -162,7 +167,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -218,7 +223,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAddItem={handleAdd} handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalUpdatePermission <ModalUpdatePermission
isOpen={showModalUpdateItem} isOpen={showModalUpdateItem}
@@ -228,7 +233,7 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
handleUpdate={handleUpdate} handleUpdate={handleUpdate}
initialValues={selectedItem ? { name: selectedItem.name } : undefined} initialValues={selectedItem ? { name: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeletePermission <ModalDeletePermission
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
@@ -239,7 +244,5 @@ export const Components: FC = (): ReactElement => {
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,14 +1,13 @@
import * as React from 'react'; import { createFileRoute } from '@tanstack/react-router'
import * as React from 'react'
import { FC, Fragment, ReactElement, useState } from 'react'; import { FC, Fragment, ReactElement, useState } from 'react'
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
AuditOutlined, AuditOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'; import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -16,19 +15,19 @@ import {
PaginationState, PaginationState,
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalProcessDelivery from './_components/modal-process-item'; import ModalProcessDelivery from './_components/prizes/modal-process-item'
type OrderValid = 'valid' | 'invalid' | 'unchecked'; type OrderValid = 'valid' | 'invalid' | 'unchecked'
type Status = 'undelivered' | 'delivered'; type Status = 'undelivered' | 'delivered'
interface Prize { interface Prize {
id: number; id: number
name: string; name: string
orderValid: OrderValid; orderValid: OrderValid
items: string; items: string
address: string; address: string
status: Status; status: Status
} }
const items = [ const items = [
@@ -38,7 +37,7 @@ const items = [
'Sticker Isi 3', 'Sticker Isi 3',
'Sticker Isi 5', 'Sticker Isi 5',
'Gelang Karet', 'Gelang Karet',
]; ]
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1, id: i + 1,
@@ -51,24 +50,28 @@ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
items: items[i % items.length], items: items[i % items.length],
address: 'Jl. Pantai Cibaduyut Indah', address: 'Jl. Pantai Cibaduyut Indah',
status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status, status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
})); }))
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/prizes')({
component: PrizesPage,
})
function PrizesPage() {
const [showModalProcessDelivery, setShowModalProcessDelivery] = const [showModalProcessDelivery, setShowModalProcessDelivery] =
useState(false); useState(false)
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false)
const deliveryOptions = [ const deliveryOptions = [
{ id: 'option1', value: 'undelivered', label: 'Undelivered' }, { id: 'option1', value: 'undelivered', label: 'Undelivered' },
{ id: 'option1', value: 'delivered', label: 'Delivered' }, { id: 'option1', value: 'delivered', label: 'Delivered' },
]; ]
const columns: ColumnDef<Prize>[] = [ const columns: ColumnDef<Prize>[] = [
{ {
@@ -102,24 +105,24 @@ export const Components: FC = (): ReactElement => {
header: 'Order Valid?', header: 'Order Valid?',
accessorKey: 'orderValid', accessorKey: 'orderValid',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.orderValid; const status = row.original.orderValid
const statusColors: Record<OrderValid, string> = { const statusColors: Record<OrderValid, string> = {
valid: 'bg-success-200 text-success-500', valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500', invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900', unchecked: 'bg-warning-200 text-warning-900',
}; }
const statusText: Record<OrderValid, string> = { const statusText: Record<OrderValid, string> = {
valid: 'Valid', valid: 'Valid',
invalid: 'Invalid', invalid: 'Invalid',
unchecked: 'Unchecked', unchecked: 'Unchecked',
}; }
return ( return (
<div <div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`} className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
> >
{statusText[status]} {statusText[status]}
</div> </div>
); )
}, },
}, },
{ {
@@ -134,22 +137,22 @@ export const Components: FC = (): ReactElement => {
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
const statusColors: Record<Status, string> = { const statusColors: Record<Status, string> = {
delivered: 'bg-success-200 text-success-500', delivered: 'bg-success-200 text-success-500',
undelivered: 'bg-danger-200 text-danger-500', undelivered: 'bg-danger-200 text-danger-500',
}; }
const statusText: Record<Status, string> = { const statusText: Record<Status, string> = {
delivered: 'Delivered', delivered: 'Delivered',
undelivered: 'Undelivered', undelivered: 'Undelivered',
}; }
return ( return (
<div <div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`} className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
> >
{statusText[status]} {statusText[status]}
</div> </div>
); )
}, },
}, },
{ {
@@ -159,8 +162,8 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setShowModalProcessDelivery(true); setShowModalProcessDelivery(true)
}} }}
className="flex items-center gap-2 w-full" className="flex items-center gap-2 w-full"
> >
@@ -168,7 +171,7 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: mockData, data: mockData,
@@ -184,7 +187,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize), pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false, manualPagination: false,
}); })
return ( return (
<Fragment> <Fragment>
@@ -219,7 +222,7 @@ export const Components: FC = (): ReactElement => {
options={deliveryOptions} options={deliveryOptions}
onClose={() => setShowFilter(false)} onClose={() => setShowFilter(false)}
onFilterChange={(value) => { onFilterChange={(value) => {
console.log('Selected filter:', value); console.log('Selected filter:', value)
}} }}
/> />
</div> </div>
@@ -234,11 +237,9 @@ export const Components: FC = (): ReactElement => {
isOpen={showModalProcessDelivery} isOpen={showModalProcessDelivery}
onClose={() => setShowModalProcessDelivery(false)} onClose={() => setShowModalProcessDelivery(false)}
handleProcessDelivery={() => { handleProcessDelivery={() => {
console.log('Action ketika user menekan tombol Proses Pengiriman'); console.log('Action ketika user menekan tombol Proses Pengiriman')
}} }}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,10 +1,11 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons"; import { createFileRoute } from '@tanstack/react-router'
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms"; import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms"; import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { cn } from "@imphnen-frontend-service/utils"; import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"; import { cn } from '@imphnen-frontend-service/utils'
import { useState } from "react"; import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap"; import { useState } from 'react'
import { ModalCreateRoadmap } from './_components/roadmap-dimentorin/modal/create-roadmap'
type LearningStatus = 'active' | 'inactive' type LearningStatus = 'active' | 'inactive'
@@ -22,19 +23,23 @@ const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
status: i % 2 === 0 ? 'active' : 'inactive', status: i % 2 === 0 ? 'active' : 'inactive',
})) }))
export default function Components(): React.ReactElement { export const Route = createFileRoute('/_authenticated/roadmap-dimentorin')({
component: RoadmapDimentorinPage,
})
function RoadmapDimentorinPage(): React.ReactElement {
const [openCreateModal, setOpenCreateModal] = useState(false) const [openCreateModal, setOpenCreateModal] = useState(false)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const columns: ColumnDef<RoadmapType>[] = [ const columns: ColumnDef<RoadmapType>[] = [
{ {
id: 'select', id: 'select',
meta: { cellClassName: cn("w-20") }, meta: { cellClassName: cn('w-20') },
header: ({ table }) => ( header: ({ table }) => (
<input <input
type="checkbox" type="checkbox"
@@ -72,34 +77,34 @@ export default function Components(): React.ReactElement {
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
const statusColors: Record<LearningStatus, string> = { const statusColors: Record<LearningStatus, string> = {
inactive: 'bg-danger-200 text-danger-700', inactive: 'bg-danger-200 text-danger-700',
active: 'bg-success-200 text-success-500', active: 'bg-success-200 text-success-500',
}; }
const statusText: Record<LearningStatus, string> = { const statusText: Record<LearningStatus, string> = {
inactive: 'Inactive', inactive: 'Inactive',
active: 'Active', active: 'Active',
}; }
return ( return (
<div <div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`} className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
> >
{statusText[status]} {statusText[status]}
</div> </div>
); )
}, },
}, },
{ {
header: 'Action', header: 'Action',
meta: { cellClassName: cn("w-72") }, meta: { cellClassName: cn('w-72') },
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex gap-[8px]"> <div className="flex gap-[8px]">
<Button <Button
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -109,7 +114,7 @@ export default function Components(): React.ReactElement {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -134,7 +139,7 @@ export default function Components(): React.ReactElement {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize), pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false, manualPagination: false,
}); })
return ( return (
<BackofficeWrapper title="Dimentorin.dev"> <BackofficeWrapper title="Dimentorin.dev">
@@ -1,12 +1,13 @@
import { FC, Fragment, ReactElement, useRef, useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { import {
SearchOutlined, SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable } from '@imphnen-frontend-service/ui/organisms'; import { DataTable } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -14,27 +15,31 @@ import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalAddRole from './_components/modal-add-role'; import ModalAddRole from './_components/roles/modal-add-role'
import ModalUpdateRole from './_components/modal-update-role'; import ModalUpdateRole from './_components/roles/modal-update-role'
import ModalDeleteRole from './_components/modal-delete-role'; import ModalDeleteRole from './_components/roles/modal-delete-role'
import { useQueryState } from '@imphnen-frontend-service/utils'; import { useQueryState } from '@imphnen-frontend-service/utils'
import { import {
useRoleList, useRoleList,
useCreateRole, useCreateRole,
useUpdateRole, useUpdateRole,
useDeleteRole, useDeleteRole,
TRolesListItem, TRolesListItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
import React from 'react'; import React from 'react'
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/roles')({
const [showModalAddItem, setShowModalAddItem] = useState(false); component: RolesPage,
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); })
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null); function RolesPage() {
const [search, setSearch] = useState(''); const [showModalAddItem, setShowModalAddItem] = useState(false)
const pendingFormData = useRef<any>(null); const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null)
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const { const {
step: currentStep, step: currentStep,
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
defaultValue: 1, defaultValue: 1,
maxValue: 2, maxValue: 2,
minValue: 1, minValue: 1,
}); })
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const { data: rolesData, isLoading } = useRoleList({ const { data: rolesData, isLoading } = useRoleList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const createRole = useCreateRole(); const createRole = useCreateRole()
const updateRole = useUpdateRole(); const updateRole = useUpdateRole()
const deleteRole = useDeleteRole(); const deleteRole = useDeleteRole()
const roles: TRolesListItem[] = rolesData?.data ?? []; const roles: TRolesListItem[] = rolesData?.data ?? []
const totalItems = rolesData?.meta?.total ?? roles.length; const totalItems = rolesData?.meta?.total ?? roles.length
const handleAdd = async (): Promise<boolean> => { const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) { if (pendingFormData.current) {
await createRole.mutateAsync(pendingFormData.current); await createRole.mutateAsync(pendingFormData.current)
} }
return true; return true
}; }
const handleUpdate = async (): Promise<boolean> => { const handleUpdate = async (): Promise<boolean> => {
if (selectedRole && pendingFormData.current) { if (selectedRole && pendingFormData.current) {
await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current }); await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current })
} }
return true; return true
}; }
const handleDelete = async (): Promise<boolean> => { const handleDelete = async (): Promise<boolean> => {
if (selectedRole) { if (selectedRole) {
await deleteRole.mutateAsync(selectedRole.id); await deleteRole.mutateAsync(selectedRole.id)
} }
return true; return true
}; }
const columns: ColumnDef<TRolesListItem>[] = [ const columns: ColumnDef<TRolesListItem>[] = [
{ {
@@ -123,9 +128,9 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedRole(row.original); setSelectedRole(row.original)
setShowModalUpdateItem(true); setShowModalUpdateItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -135,9 +140,9 @@ export const Components: FC = (): ReactElement => {
variant="danger" variant="danger"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedRole(row.original); setSelectedRole(row.original)
setShowModalDeleteItem(true); setShowModalDeleteItem(true)
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -146,7 +151,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: roles, data: roles,
@@ -162,7 +167,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<Fragment> <Fragment>
@@ -218,7 +223,7 @@ export const Components: FC = (): ReactElement => {
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
handleAdd={handleAdd} handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalUpdateRole <ModalUpdateRole
isOpen={showModalUpdateItem} isOpen={showModalUpdateItem}
@@ -228,7 +233,7 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
handleUpdate={handleUpdate} handleUpdate={handleUpdate}
initialValues={selectedRole ? { name: selectedRole.name } : undefined} initialValues={selectedRole ? { name: selectedRole.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }} onDataCapture={(data) => { pendingFormData.current = data }}
/> />
<ModalDeleteRole <ModalDeleteRole
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
@@ -239,7 +244,5 @@ export const Components: FC = (): ReactElement => {
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,33 +1,38 @@
import { SearchOutlined } from "@ant-design/icons"; import { createFileRoute } from '@tanstack/react-router'
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms"; import { SearchOutlined } from '@ant-design/icons'
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms"; import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { cn } from "@imphnen-frontend-service/utils"; import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"; import { cn } from '@imphnen-frontend-service/utils'
import { ReactElement, useState } from "react"; import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
import { ModalDetailSession } from "./_components/modal/detail"; import { ReactElement, useState } from 'react'
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service"; import { ModalDetailSession } from './_components/session-dimentorin/modal/detail'
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
export default function Components(): ReactElement { export const Route = createFileRoute('/_authenticated/session-dimentorin')({
const [openDetail, setOpenDetail] = useState(false); component: SessionDimentorinPage,
const [statusFilter, setStatusFilter] = useState(''); })
function SessionDimentorinPage(): ReactElement {
const [openDetail, setOpenDetail] = useState(false)
const [statusFilter, setStatusFilter] = useState('')
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const { data: sessionsData, isLoading } = useMySessions( const { data: sessionsData, isLoading } = useMySessions(
statusFilter ? { status: statusFilter } : undefined statusFilter ? { status: statusFilter } : undefined
); )
const sessions: TSessionListItem[] = sessionsData?.sessions ?? []; const sessions: TSessionListItem[] = sessionsData?.sessions ?? []
const totalItems = sessionsData?.total ?? sessions.length; const totalItems = sessionsData?.total ?? sessions.length
const columns: ColumnDef<TSessionListItem>[] = [ const columns: ColumnDef<TSessionListItem>[] = [
{ {
id: 'select', id: 'select',
meta: { cellClassName: cn("w-20") }, meta: { cellClassName: cn('w-20') },
header: ({ table }) => ( header: ({ table }) => (
<input <input
type="checkbox" type="checkbox"
@@ -73,31 +78,31 @@ export default function Components(): ReactElement {
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
pending: 'bg-warning-200 text-warning-700', pending: 'bg-warning-200 text-warning-700',
confirmed: 'bg-primary-200 text-primary-700', confirmed: 'bg-primary-200 text-primary-700',
ongoing: 'bg-warning-200 text-warning-700', ongoing: 'bg-warning-200 text-warning-700',
completed: 'bg-success-200 text-success-500', completed: 'bg-success-200 text-success-500',
cancelled: 'bg-danger-200 text-danger-500', cancelled: 'bg-danger-200 text-danger-500',
}; }
return ( return (
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}> <div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status} {status}
</div> </div>
); )
}, },
}, },
{ {
header: 'Action', header: 'Action',
meta: { cellClassName: cn("w-52") }, meta: { cellClassName: cn('w-52') },
cell: () => ( cell: () => (
<Button <Button
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setOpenDetail(true); setOpenDetail(true)
}} }}
className="flex items-center gap-2 w-max" className="flex items-center gap-2 w-max"
> >
@@ -121,7 +126,7 @@ export default function Components(): ReactElement {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(totalItems / pagination.pageSize), pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<BackofficeWrapper title="Dimentorin.dev"> <BackofficeWrapper title="Dimentorin.dev">
@@ -1,22 +1,27 @@
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms" import { createFileRoute } from '@tanstack/react-router'
import { cn, For } from "@imphnen-frontend-service/utils" import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
import { useState } from "react" import { cn, For } from '@imphnen-frontend-service/utils'
import { GeneralSettings } from "./_components/general" import { useState } from 'react'
import { UserRolesPermission } from "./_components/user-roles-permission" import { GeneralSettings } from './_components/settings-dimentorin/general'
import { NotificationSettings } from "./_components/notification" import { UserRolesPermission } from './_components/settings-dimentorin/user-roles-permission'
import { SecuritySettings } from "./_components/security" import { NotificationSettings } from './_components/settings-dimentorin/notification'
import { PaymentSettings } from "./_components/payment" import { SecuritySettings } from './_components/settings-dimentorin/security'
import { PaymentSettings } from './_components/settings-dimentorin/payment'
const TABS = { const TABS = {
general: "General Settings", general: 'General Settings',
userRolePermissions: "User Roles & Permissions", userRolePermissions: 'User Roles & Permissions',
notification: "Notification Settings", notification: 'Notification Settings',
security: "Security", security: 'Security',
payment: "Payment", payment: 'Payment',
} as const } as const
type Tabs = typeof TABS[keyof typeof TABS] type Tabs = typeof TABS[keyof typeof TABS]
export default function Components(): React.ReactElement { export const Route = createFileRoute('/_authenticated/settings-dimentorin')({
component: SettingsDimentorinPage,
})
function SettingsDimentorinPage(): React.ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general) const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
return ( return (
@@ -30,8 +35,8 @@ export default function Components(): React.ReactElement {
<button <button
key={tab} key={tab}
className={cn( className={cn(
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100", 'px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100',
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600" activeTab === tab && 'bg-primary-500 text-white hover:bg-primary-600'
)} )}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
> >
@@ -1,13 +1,13 @@
import * as React from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useState } from 'react'; import * as React from 'react'
import { FC, Fragment, ReactElement, useState } from 'react'
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
AuditOutlined, AuditOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'; import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -15,16 +15,16 @@ import {
PaginationState, PaginationState,
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import ModalValidate from './_components/modal-validate'; import ModalValidate from './_components/transactions/modal-validate'
type TransactionStatus = 'valid' | 'invalid' | 'unchecked'; type TransactionStatus = 'valid' | 'invalid' | 'unchecked'
interface Transaction { interface Transaction {
id: number; id: number
name: string; name: string
transactionNumber: string; transactionNumber: string
status: TransactionStatus; status: TransactionStatus
} }
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
@@ -36,24 +36,28 @@ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
: i % 5 === 0 : i % 5 === 0
? 'unchecked' ? 'unchecked'
: 'valid') as TransactionStatus, : 'valid') as TransactionStatus,
})); }))
export const Components: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated/transactions')({
const [showModalValidate, setShowModalValidate] = useState(false); component: TransactionsPage,
})
function TransactionsPage() {
const [showModalValidate, setShowModalValidate] = useState(false)
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false)
const validationOptions = [ const validationOptions = [
{ id: 'option1', value: 'unchecked', label: 'Unchecked' }, { id: 'option1', value: 'unchecked', label: 'Unchecked' },
{ id: 'option2', value: 'valid', label: 'Valid' }, { id: 'option2', value: 'valid', label: 'Valid' },
{ id: 'option3', value: 'invalid', label: 'Invalid' }, { id: 'option3', value: 'invalid', label: 'Invalid' },
]; ]
const columns: ColumnDef<Transaction>[] = [ const columns: ColumnDef<Transaction>[] = [
{ {
@@ -91,24 +95,24 @@ export const Components: FC = (): ReactElement => {
header: 'Order Valid?', header: 'Order Valid?',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
const statusColors: Record<TransactionStatus, string> = { const statusColors: Record<TransactionStatus, string> = {
valid: 'bg-success-200 text-success-500', valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500', invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900', unchecked: 'bg-warning-200 text-warning-900',
}; }
const statusText: Record<TransactionStatus, string> = { const statusText: Record<TransactionStatus, string> = {
valid: 'Valid', valid: 'Valid',
invalid: 'Invalid', invalid: 'Invalid',
unchecked: 'Unchecked', unchecked: 'Unchecked',
}; }
return ( return (
<div <div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`} className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
> >
{statusText[status]} {statusText[status]}
</div> </div>
); )
}, },
}, },
{ {
@@ -118,8 +122,8 @@ export const Components: FC = (): ReactElement => {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setShowModalValidate(true); setShowModalValidate(true)
}} }}
className="flex items-center gap-2 w-full" className="flex items-center gap-2 w-full"
> >
@@ -127,7 +131,7 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
), ),
}, },
]; ]
const table = useReactTable({ const table = useReactTable({
data: mockTransactions, data: mockTransactions,
@@ -143,7 +147,7 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize), pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
manualPagination: false, manualPagination: false,
}); })
return ( return (
<Fragment> <Fragment>
@@ -180,7 +184,7 @@ export const Components: FC = (): ReactElement => {
options={validationOptions} options={validationOptions}
onClose={() => setShowFilter(false)} onClose={() => setShowFilter(false)}
onFilterChange={(value) => { onFilterChange={(value) => {
console.log('Selected filter:', value); console.log('Selected filter:', value)
}} }}
/> />
</div> </div>
@@ -195,14 +199,12 @@ export const Components: FC = (): ReactElement => {
isOpen={showModalValidate} isOpen={showModalValidate}
onClose={() => setShowModalValidate(false)} onClose={() => setShowModalValidate(false)}
handleValid={() => { handleValid={() => {
console.log('Action ketika user klik Valid'); console.log('Action ketika user klik Valid')
}} }}
handleInvalid={() => { handleInvalid={() => {
console.log('Action ketika user klik Tidak Valid'); console.log('Action ketika user klik Tidak Valid')
}} }}
/> />
</Fragment> </Fragment>
); )
}; }
export default Components;
@@ -1,10 +1,11 @@
import { SearchOutlined } from '@ant-design/icons'; import { createFileRoute } from '@tanstack/react-router'
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'; import { SearchOutlined } from '@ant-design/icons'
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { import {
BackofficeWrapper, BackofficeWrapper,
DataTable, DataTable,
} from '@imphnen-frontend-service/ui/organisms'; } from '@imphnen-frontend-service/ui/organisms'
import { cn, For } from '@imphnen-frontend-service/utils'; import { cn, For } from '@imphnen-frontend-service/utils'
import { import {
ColumnDef, ColumnDef,
getCoreRowModel, getCoreRowModel,
@@ -12,48 +13,52 @@ import {
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table'
import { ReactElement, useState } from 'react'; import { ReactElement, useState } from 'react'
import { ModalDetailUser } from './_components/modal/detail'; import { ModalDetailUser } from './_components/users-dimentorin/modal/detail'
import { import {
useMentorList, useMentorList,
useUserList, useUserList,
MentorDetailResponseDto, MentorDetailResponseDto,
TUsersListItem, TUsersListItem,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service'
export default function Components(): ReactElement { export const Route = createFileRoute('/_authenticated/users-dimentorin')({
const TABS = ['mentor', 'mentee'] as const; component: UsersDimentorinPage,
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor'); })
const [showDetail, setShowDetail] = useState(false);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); function UsersDimentorinPage(): ReactElement {
const TABS = ['mentor', 'mentee'] as const
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
const [showDetail, setShowDetail] = useState(false)
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const [search, setSearch] = useState('')
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
}); })
const { data: mentorData, isLoading: mentorLoading } = useMentorList({ const { data: mentorData, isLoading: mentorLoading } = useMentorList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const { data: menteeData, isLoading: menteeLoading } = useUserList({ const { data: menteeData, isLoading: menteeLoading } = useUserList({
search, search,
page: pagination.pageIndex + 1, page: pagination.pageIndex + 1,
per_page: pagination.pageSize, per_page: pagination.pageSize,
}); })
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? []; const mentors: MentorDetailResponseDto[] = mentorData?.data ?? []
const mentees: TUsersListItem[] = menteeData?.data ?? []; const mentees: TUsersListItem[] = menteeData?.data ?? []
const mentorTotal = mentorData?.meta?.total ?? mentors.length; const mentorTotal = mentorData?.meta?.total ?? mentors.length
const menteeTotal = menteeData?.meta?.total ?? mentees.length; const menteeTotal = menteeData?.meta?.total ?? mentees.length
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading; const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal; const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal
const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [ const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [
{ {
@@ -97,17 +102,17 @@ export default function Components(): ReactElement {
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
active: 'bg-success-200 text-success-500', active: 'bg-success-200 text-success-500',
pending: 'bg-warning-200 text-warning-700', pending: 'bg-warning-200 text-warning-700',
inactive: 'bg-danger-200 text-danger-500', inactive: 'bg-danger-200 text-danger-500',
}; }
return ( return (
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}> <div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status} {status}
</div> </div>
); )
}, },
}, },
{ {
@@ -118,9 +123,9 @@ export default function Components(): ReactElement {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedUserId(row.original.id); setSelectedUserId(row.original.id)
setShowDetail(true); setShowDetail(true)
}} }}
className="flex items-center gap-2 w-max" className="flex items-center gap-2 w-max"
> >
@@ -128,7 +133,7 @@ export default function Components(): ReactElement {
</Button> </Button>
), ),
}, },
]; ]
const menteeColumns: ColumnDef<TUsersListItem>[] = [ const menteeColumns: ColumnDef<TUsersListItem>[] = [
{ {
@@ -179,9 +184,9 @@ export default function Components(): ReactElement {
variant="primary" variant="primary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
setSelectedUserId(row.original.id); setSelectedUserId(row.original.id)
setShowDetail(true); setShowDetail(true)
}} }}
className="flex items-center gap-2 w-max" className="flex items-center gap-2 w-max"
> >
@@ -189,7 +194,7 @@ export default function Components(): ReactElement {
</Button> </Button>
), ),
}, },
]; ]
const mentorTable = useReactTable({ const mentorTable = useReactTable({
data: mentors, data: mentors,
@@ -202,7 +207,7 @@ export default function Components(): ReactElement {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(mentorTotal / pagination.pageSize), pageCount: Math.ceil(mentorTotal / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
const menteeTable = useReactTable({ const menteeTable = useReactTable({
data: mentees, data: mentees,
@@ -215,7 +220,7 @@ export default function Components(): ReactElement {
onPaginationChange: setPagination, onPaginationChange: setPagination,
pageCount: Math.ceil(menteeTotal / pagination.pageSize), pageCount: Math.ceil(menteeTotal / pagination.pageSize),
manualPagination: true, manualPagination: true,
}); })
return ( return (
<BackofficeWrapper title="Dimentorin.dev"> <BackofficeWrapper title="Dimentorin.dev">
@@ -234,8 +239,8 @@ export default function Components(): ReactElement {
activeTab === tab && 'bg-white' activeTab === tab && 'bg-white'
)} )}
onClick={() => { onClick={() => {
setActiveTab(tab); setActiveTab(tab)
setPagination((p) => ({ ...p, pageIndex: 0 })); setPagination((p) => ({ ...p, pageIndex: 0 }))
}} }}
> >
{tab} {tab}
@@ -275,5 +280,5 @@ export default function Components(): ReactElement {
userId={selectedUserId} userId={selectedUserId}
/> />
</BackofficeWrapper> </BackofficeWrapper>
); )
} }
+20
View File
@@ -0,0 +1,20 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_public')({
beforeLoad: () => {
const session = SessionToken.get()
if (session?.token?.access_token) {
throw redirect({ to: '/hackathon-dashboard' })
}
},
component: PublicLayout,
})
function PublicLayout() {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
)
}
@@ -5,7 +5,7 @@ import {
useBackofficeLogin, useBackofficeLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate } from 'react-router'; import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
export const useLogin = () => { export const useLogin = () => {
@@ -25,7 +25,7 @@ export const useLogin = () => {
try { try {
await loginMutation.mutateAsync(data); await loginMutation.mutateAsync(data);
toast.success('Login berhasil!'); toast.success('Login berhasil!');
navigate('/hackathon-dashboard'); navigate({ to: '/hackathon-dashboard' });
} catch (error) { } catch (error) {
console.error('[Backoffice Login] Error:', error); console.error('[Backoffice Login] Error:', error);
toast.error((error as Error).message || 'Login gagal'); toast.error((error as Error).message || 'Login gagal');
@@ -1,33 +1,38 @@
import { useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'
import { useLogin, authLoginSchema, TLoginRequest } from '@imphnen-frontend-service/service'; import { useState } from 'react'
import { useNavigate } from 'react-router'; import { useLogin, authLoginSchema, TLoginRequest } from '@imphnen-frontend-service/service'
import { useForm } from 'react-hook-form'; import { useNavigate } from '@tanstack/react-router'
import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'
import { toast } from 'sonner'; import { zodResolver } from '@hookform/resolvers/zod'
import { Icon } from '@iconify/react'; import { toast } from 'sonner'
import { Icon } from '@iconify/react'
export default function LoginPage() { export const Route = createFileRoute('/_public/auth/login')({
const navigate = useNavigate(); component: LoginPage,
const loginMutation = useLogin(); })
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null); function LoginPage() {
const navigate = useNavigate()
const loginMutation = useLogin()
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState<string | null>(null)
const { register, handleSubmit, formState: { errors, isValid } } = useForm<TLoginRequest>({ const { register, handleSubmit, formState: { errors, isValid } } = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'onChange', mode: 'onChange',
defaultValues: { email: '', password: '' }, defaultValues: { email: '', password: '' },
}); })
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
setError(null); setError(null)
try { try {
await loginMutation.mutateAsync(data); await loginMutation.mutateAsync(data)
toast.success('Login successful!'); toast.success('Login successful!')
navigate('/'); navigate({ to: '/' })
} catch (err) { } catch (err) {
setError((err as Error).message || 'Login failed'); setError((err as Error).message || 'Login failed')
} }
}); })
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
@@ -89,5 +94,5 @@ export default function LoginPage() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
+12
View File
@@ -0,0 +1,12 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/')({
beforeLoad: () => {
const session = SessionToken.get()
if (session?.token?.access_token) {
throw redirect({ to: '/hackathon-dashboard' })
}
throw redirect({ to: '/auth/login' })
},
})
+9 -1
View File
@@ -3,6 +3,7 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
export default defineConfig(() => ({ export default defineConfig(() => ({
root: __dirname, root: __dirname,
@@ -15,7 +16,14 @@ export default defineConfig(() => ({
port: 3001, port: 3001,
host: 'localhost', host: 'localhost',
}, },
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], plugins: [
TanStackRouterVite({
routeFileIgnorePattern: '_components|_hooks|_hook|_data',
}),
react(),
nxViteTsPaths(),
nxCopyAssetsPlugin(['*.md']),
],
build: { build: {
outDir: '../../dist/apps/backoffice', outDir: '../../dist/apps/backoffice',
emptyOutDir: true, emptyOutDir: true,
@@ -1,20 +0,0 @@
import { FC, ReactElement } from 'react';
import { HeroSection } from './_components/hero-section';
import { WhatWeOfferSection } from './_components/what-we-offer-section';
import { TestimonialSection } from './_components/testimonial-section';
import { FAQSection } from './_components/faq-section';
import { CTASection } from './_components/cta-section';
export const Components: FC = (): ReactElement => {
return (
<>
<HeroSection />
<WhatWeOfferSection />
<TestimonialSection />
<FAQSection />
<CTASection />
</>
);
};
export default Components;
@@ -1,15 +0,0 @@
import { Outlet } from "react-router-dom";
import { Header } from "./_components/header";
import { Footer } from "./_components/footer";
export default function Layout() {
return (
<div className="flex min-h-screen flex-col bg-primary-50">
<Header />
<main className="flex-1">
<Outlet />
</main>
<Footer />
</div>
);
}
@@ -1,14 +0,0 @@
import { ReactElement } from "react";
import { Contribute } from "./_components/contribute";
import { WorkWithUs } from "./_components/work-with-us";
import { Collaborate } from "./_components/collaborate";
export default function Components(): ReactElement {
return (
<main>
<Contribute />
<WorkWithUs />
<Collaborate />
</main>
)
}
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,80 +0,0 @@
import { FC, ReactElement, useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useGoogleCallback, useAuthStore } from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
export const GoogleCallbackPage: FC = (): ReactElement => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { setSession, clearSession } = useAuthStore();
const { mutate: googleCallback } = useGoogleCallback();
useEffect(() => {
const handleCallback = async () => {
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
if (error) {
toast.error('Google login dibatalkan atau terjadi kesalahan');
navigate('/auth/login');
return;
}
if (!code || !state) {
toast.error('Parameter login Google tidak valid');
navigate('/auth/login');
return;
}
try {
googleCallback(
{ code, state },
{
onSuccess: (response) => {
if (response.token && response.user) {
setSession({
token: response.token,
user: response.user,
});
toast.success('Login Google berhasil!');
navigate('/dashboard');
} else {
throw new Error('Response data tidak valid');
}
},
onError: (error) => {
console.error('Google OAuth callback error:', error);
toast.error('Login Google gagal');
clearSession();
navigate('/auth/login');
},
}
);
} catch (error) {
console.error('Google OAuth callback error:', error);
toast.error('Login Google gagal');
clearSession();
navigate('/auth/login');
}
};
handleCallback();
}, [searchParams, navigate, setSession, clearSession, googleCallback]);
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-500 mx-auto mb-4"></div>
<h2 className="text-2xl font-semibold text-primary-500 mb-2">
Menyelesaikan Login Google...
</h2>
<p className="text-gray-600">
Mohon tunggu sebentar, kami sedang memproses login Anda.
</p>
</div>
</div>
);
};
export default GoogleCallbackPage;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
-10
View File
@@ -1,10 +0,0 @@
import { Outlet, ScrollRestoration } from "react-router-dom";
export default function RootLayout() {
return (
<>
<Outlet />
<ScrollRestoration />
</>
)
}
+16 -32
View File
@@ -1,37 +1,22 @@
import { createRoot } from 'react-dom/client'; import { StrictMode } from 'react'
import { middleware } from './middleware'; import { createRoot } from 'react-dom/client'
import { StrictMode } from 'react'; import { RouterProvider, createRouter } from '@tanstack/react-router'
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router'; import { ModalLoginProvider, QueryProvider } from '@imphnen-frontend-service/utils'
import { import { Toaster } from 'sonner'
add404PageToRoutesChildren, import { routeTree } from './routeTree.gen'
addErrorElementToRoutes, import './index.css'
convertPagesToRoute,
ModalLoginProvider,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const router = createRouter({ routeTree })
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject; declare module '@tanstack/react-router' {
addErrorElementToRoutes(errorFiles, routes); interface Register {
add404PageToRoutesChildren(notFoundFiles, routes); router: typeof router
}
}
const router = createBrowserRouter([ const rootElement = document.getElementById('root')
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element')
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
@@ -42,5 +27,4 @@ createRoot(rootElement).render(
</ModalLoginProvider> </ModalLoginProvider>
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
); )
// force deploy
-118
View File
@@ -1,118 +0,0 @@
import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/forgot/otp',
'/auth/register',
'/auth/register/otp',
'/auth/register/success',
'/auth/new-password',
'/auth/register-mentor',
'/auth/register-mentor/pending',
'/auth/register-mentor/success',
'/auth/google-callback',
'/auth/google-oauth-popup',
'/resources',
];const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
const mappingPublicPrefixRoutes = [
'/mentoring',
'/articles',
]
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
return null;
}
if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null;
};
+25
View File
@@ -0,0 +1,25 @@
/* prettier-ignore-start */
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file is auto-generated by TanStack Router
// Import Routes
import { Route as rootRoute } from './routes/__root'
// Create/Update Routes
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {}
}
// Create and export the route tree
export const routeTree = rootRoute
+10
View File
@@ -0,0 +1,10 @@
import { createRootRoute, Outlet, ScrollRestoration } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<ScrollRestoration />
</>
),
})
@@ -0,0 +1,16 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: () => {
const session = SessionToken.get()
if (!session?.token?.access_token) {
throw redirect({ to: '/auth/login' })
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
return <Outlet />
}

Some files were not shown because too many files have changed in this diff Show More