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:
co-authored by
Claude Opus 4.6
parent
86d9e759a6
commit
4240e8eb51
@@ -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,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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// This file is auto-generated by TanStack Router
|
||||||
|
export const routeTree = {} as any
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: () => <Outlet />,
|
||||||
|
})
|
||||||
+18
-11
@@ -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;
|
|
||||||
+37
-36
@@ -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;
|
|
||||||
+53
-50
@@ -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;
|
|
||||||
+55
-52
@@ -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;
|
|
||||||
+28
-23
@@ -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>
|
||||||
+47
-44
@@ -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;
|
|
||||||
+26
-21
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
+55
-54
@@ -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;
|
|
||||||
+18
-15
@@ -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;
|
|
||||||
+84
-73
@@ -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;
|
|
||||||
+93
-83
@@ -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;
|
|
||||||
+105
-95
@@ -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;
|
|
||||||
+53
-50
@@ -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;
|
|
||||||
+44
-43
@@ -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;
|
|
||||||
+23
-18
@@ -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">
|
||||||
+53
-50
@@ -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;
|
|
||||||
+28
-23
@@ -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">
|
||||||
+21
-16
@@ -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)}
|
||||||
>
|
>
|
||||||
+37
-35
@@ -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;
|
|
||||||
+45
-40
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -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');
|
||||||
+25
-20
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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' })
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Outlet, ScrollRestoration } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Outlet />
|
|
||||||
<ScrollRestoration />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user