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 { middleware } from './middleware';
|
||||
import { StrictMode } from 'react';
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
||||
import {
|
||||
add404PageToRoutesChildren,
|
||||
addErrorElementToRoutes,
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { QueryProvider } from '@imphnen-frontend-service/utils'
|
||||
import { Toaster } from 'sonner'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import './index.css'
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
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 router = createRouter({ routeTree })
|
||||
|
||||
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
||||
addErrorElementToRoutes(errorFiles, routes);
|
||||
add404PageToRoutesChildren(notFoundFiles, routes);
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
...routes,
|
||||
loader: middleware,
|
||||
shouldRevalidate: () => true,
|
||||
},
|
||||
]);
|
||||
const rootElement = document.getElementById('root')
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" />
|
||||
</QueryProvider>
|
||||
</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 { Outlet } from 'react-router-dom';
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
import { SessionToken } from '@imphnen-frontend-service/service'
|
||||
import { useState } from 'react'
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
export const Route = createFileRoute('/_authenticated')({
|
||||
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 (
|
||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||
@@ -13,14 +24,12 @@ export const AppLayout: FC = (): ReactElement => {
|
||||
onClose={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
||||
<header
|
||||
className={
|
||||
'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')
|
||||
}
|
||||
>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
)
|
||||
}
|
||||
+37
-36
@@ -1,14 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import * as React from 'react'
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
|
||||
import {
|
||||
FilterOutlined,
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -16,20 +15,24 @@ import {
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalEditAccount from './_components/modal-edit-account';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalEditAccount from './_components/accounts/modal-edit-account'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useUserList,
|
||||
useUpdateUserById,
|
||||
TUsersListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const pendingFormData = useRef<any>(null);
|
||||
export const Route = createFileRoute('/_authenticated/accounts')({
|
||||
component: AccountsPage,
|
||||
})
|
||||
|
||||
function AccountsPage() {
|
||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false)
|
||||
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
const pendingFormData = useRef<any>(null)
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -40,31 +43,31 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
|
||||
const { data: usersData, isLoading } = useUserList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const updateUser = useUpdateUserById();
|
||||
})
|
||||
const updateUser = useUpdateUserById()
|
||||
|
||||
const users: TUsersListItem[] = usersData?.data ?? [];
|
||||
const totalItems = usersData?.meta?.total ?? users.length;
|
||||
const users: TUsersListItem[] = usersData?.data ?? []
|
||||
const totalItems = usersData?.meta?.total ?? users.length
|
||||
|
||||
const handleEditAccount = async () => {
|
||||
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>[] = [
|
||||
{
|
||||
@@ -118,9 +121,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedUser(row.original);
|
||||
setShowModalEditAccount(true);
|
||||
e.stopPropagation()
|
||||
setSelectedUser(row.original)
|
||||
setShowModalEditAccount(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -128,7 +131,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: users,
|
||||
@@ -144,7 +147,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -200,10 +203,8 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
</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 {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -14,27 +15,31 @@ import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddEvent from './_components/modal-add-event';
|
||||
import ModalUpdateEvent from './_components/modal-update-event';
|
||||
import ModalDeleteEvent from './_components/modal-delete-event';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalAddEvent from './_components/cms-events/modal-add-event'
|
||||
import ModalUpdateEvent from './_components/cms-events/modal-update-event'
|
||||
import ModalDeleteEvent from './_components/cms-events/modal-delete-event'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useEventList,
|
||||
useCreateEvent,
|
||||
useUpdateEvent,
|
||||
useDeleteEvent,
|
||||
TEventsListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import React from 'react';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import React from 'react'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/cms-events')({
|
||||
component: CmsEventsPage,
|
||||
})
|
||||
|
||||
function CmsEventsPage() {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false)
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
|
||||
const { data: eventsData, isLoading } = useEventList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createEvent = useCreateEvent();
|
||||
const updateEvent = useUpdateEvent();
|
||||
const deleteEvent = useDeleteEvent();
|
||||
})
|
||||
const createEvent = useCreateEvent()
|
||||
const updateEvent = useUpdateEvent()
|
||||
const deleteEvent = useDeleteEvent()
|
||||
|
||||
const events: TEventsListItem[] = eventsData?.data ?? [];
|
||||
const totalItems = eventsData?.meta?.total ?? events.length;
|
||||
const events: TEventsListItem[] = eventsData?.data ?? []
|
||||
const totalItems = eventsData?.meta?.total ?? events.length
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
await createEvent.mutateAsync(pendingFormData.current);
|
||||
await createEvent.mutateAsync(pendingFormData.current)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
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> => {
|
||||
if (selectedEvent) {
|
||||
await deleteEvent.mutateAsync(selectedEvent.id);
|
||||
await deleteEvent.mutateAsync(selectedEvent.id)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const columns: ColumnDef<TEventsListItem>[] = [
|
||||
{
|
||||
@@ -157,9 +162,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEvent(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedEvent(row.original)
|
||||
setShowModalUpdateItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -169,9 +174,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEvent(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedEvent(row.original)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -180,7 +185,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: events,
|
||||
@@ -196,7 +201,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -252,7 +257,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAdd={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalUpdateEvent
|
||||
isOpen={showModalUpdateItem}
|
||||
@@ -271,7 +276,7 @@ export const Components: FC = (): ReactElement => {
|
||||
end_date: selectedEvent.end_date,
|
||||
is_online: selectedEvent.is_online,
|
||||
} : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalDeleteEvent
|
||||
isOpen={showModalDeleteItem}
|
||||
@@ -282,7 +287,5 @@ export const Components: FC = (): ReactElement => {
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</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 {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -14,27 +15,31 @@ import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddTestimonial from './_components/modal-add-testimonial';
|
||||
import ModalUpdateTestimonial from './_components/modal-update-testimonial';
|
||||
import ModalDeleteTestimonial from './_components/modal-delete-testimonial';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalAddTestimonial from './_components/cms-testimonials/modal-add-testimonial'
|
||||
import ModalUpdateTestimonial from './_components/cms-testimonials/modal-update-testimonial'
|
||||
import ModalDeleteTestimonial from './_components/cms-testimonials/modal-delete-testimonial'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useTestimonialList,
|
||||
useCreateTestimonial,
|
||||
useUpdateTestimonial,
|
||||
useDeleteTestimonial,
|
||||
TTestimonialsListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import React from 'react';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import React from 'react'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/cms-testimonials')({
|
||||
component: CmsTestimonialsPage,
|
||||
})
|
||||
|
||||
function CmsTestimonialsPage() {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false)
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
|
||||
const { data: testimonialsData, isLoading } = useTestimonialList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createTestimonial = useCreateTestimonial();
|
||||
const updateTestimonial = useUpdateTestimonial();
|
||||
const deleteTestimonial = useDeleteTestimonial();
|
||||
})
|
||||
const createTestimonial = useCreateTestimonial()
|
||||
const updateTestimonial = useUpdateTestimonial()
|
||||
const deleteTestimonial = useDeleteTestimonial()
|
||||
|
||||
const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? [];
|
||||
const totalItems = testimonialsData?.meta?.total ?? testimonials.length;
|
||||
const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []
|
||||
const totalItems = testimonialsData?.meta?.total ?? testimonials.length
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
await createTestimonial.mutateAsync(pendingFormData.current);
|
||||
await createTestimonial.mutateAsync(pendingFormData.current)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
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> => {
|
||||
if (selectedTestimonial) {
|
||||
await deleteTestimonial.mutateAsync(selectedTestimonial.id);
|
||||
await deleteTestimonial.mutateAsync(selectedTestimonial.id)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const columns: ColumnDef<TTestimonialsListItem>[] = [
|
||||
{
|
||||
@@ -119,8 +124,8 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Content',
|
||||
accessorKey: 'content',
|
||||
cell: ({ row }) => {
|
||||
const content = row.original.content;
|
||||
return content.length > 80 ? `${content.substring(0, 80)}...` : content;
|
||||
const content = row.original.content
|
||||
return content.length > 80 ? `${content.substring(0, 80)}...` : content
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -141,9 +146,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedTestimonial(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedTestimonial(row.original)
|
||||
setShowModalUpdateItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -153,9 +158,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedTestimonial(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedTestimonial(row.original)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -164,7 +169,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: testimonials,
|
||||
@@ -180,7 +185,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -236,7 +241,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAdd={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalUpdateTestimonial
|
||||
isOpen={showModalUpdateItem}
|
||||
@@ -249,7 +254,7 @@ export const Components: FC = (): ReactElement => {
|
||||
role: selectedTestimonial.role,
|
||||
content: selectedTestimonial.content,
|
||||
} : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalDeleteTestimonial
|
||||
isOpen={showModalDeleteItem}
|
||||
@@ -260,7 +265,5 @@ export const Components: FC = (): ReactElement => {
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+28
-23
@@ -1,20 +1,25 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { For } from "@imphnen-frontend-service/utils";
|
||||
import { ReactElement } from "react";
|
||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
||||
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service";
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { For } from '@imphnen-frontend-service/utils'
|
||||
import { ReactElement } from 'react'
|
||||
import { UserGrowthChart } from './_components/dashboard-dimentorin/chart/user-growth'
|
||||
import { SessionStatusChart } from './_components/dashboard-dimentorin/chart/session-status'
|
||||
import { useMentorList, useUserList, useMySessions } from '@imphnen-frontend-service/service'
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' });
|
||||
const { data: userData } = useUserList({ per_page: 1 });
|
||||
const { data: sessionsData } = useMySessions();
|
||||
export const Route = createFileRoute('/_authenticated/dashboard-dimentorin')({
|
||||
component: DashboardDimentorinPage,
|
||||
})
|
||||
|
||||
const totalMentors = mentorData?.meta?.total ?? 0;
|
||||
const totalUsers = userData?.meta?.total ?? 0;
|
||||
const totalSessions = sessionsData?.total ?? 0;
|
||||
const topMentors = mentorData?.data ?? [];
|
||||
function DashboardDimentorinPage(): ReactElement {
|
||||
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' })
|
||||
const { data: userData } = useUserList({ per_page: 1 })
|
||||
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 = [
|
||||
{ label: 'Total Users', value: totalUsers },
|
||||
@@ -22,7 +27,7 @@ export default function Components(): ReactElement {
|
||||
{ label: 'Total Sessions', value: totalSessions },
|
||||
{ label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length },
|
||||
{ label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 },
|
||||
];
|
||||
]
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="Dimentorin.dev">
|
||||
@@ -113,20 +118,20 @@ export default function Components(): ReactElement {
|
||||
|
||||
<tbody>
|
||||
{(() => {
|
||||
const sessions = sessionsData?.sessions ?? [];
|
||||
const topicCount: Record<string, number> = {};
|
||||
const sessions = sessionsData?.sessions ?? []
|
||||
const topicCount: Record<string, number> = {}
|
||||
sessions.forEach((s) => {
|
||||
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1;
|
||||
});
|
||||
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1
|
||||
})
|
||||
const topTopics = Object.entries(topicCount)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5);
|
||||
.slice(0, 5)
|
||||
if (topTopics.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
|
||||
</tr>
|
||||
);
|
||||
)
|
||||
}
|
||||
return topTopics.map(([topic, count], index) => (
|
||||
<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">{count}</td>
|
||||
</tr>
|
||||
));
|
||||
))
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
+47
-44
@@ -1,16 +1,17 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalEditItem from './_components/modal-edit-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@ant-design/icons'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
|
||||
import ModalAddItem from './_components/dashboard/modal-add-item'
|
||||
import ModalEditItem from './_components/dashboard/modal-edit-item'
|
||||
import ModalDeleteItem from './_components/dashboard/modal-delete-item'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useUserList,
|
||||
useGachaItemList,
|
||||
@@ -18,14 +19,18 @@ import {
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/dashboard')({
|
||||
component: DashboardPage,
|
||||
})
|
||||
|
||||
function DashboardPage() {
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -36,20 +41,20 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: usersData } = useUserList({ per_page: 1 });
|
||||
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 });
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
const { data: usersData } = useUserList({ per_page: 1 })
|
||||
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 })
|
||||
const createItem = useCreateGachaItem()
|
||||
const updateItem = useUpdateGachaItem()
|
||||
const deleteItem = useDeleteGachaItem()
|
||||
|
||||
const totalUsers = usersData?.meta?.total ?? 0;
|
||||
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [];
|
||||
const totalUsers = usersData?.meta?.total ?? 0
|
||||
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? []
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
const { itemName, quantity } = pendingFormData.current
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
@@ -61,28 +66,28 @@ export const Components: FC = (): ReactElement => {
|
||||
weight: 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleEdit = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
const { itemName, quantity } = pendingFormData.current
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: { name: itemName, stock: quantity },
|
||||
});
|
||||
})
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleDelete = async (): Promise<boolean> => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
await deleteItem.mutateAsync(selectedItem.id)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -181,8 +186,8 @@ export const Components: FC = (): ReactElement => {
|
||||
size="sm"
|
||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalEditItem(true);
|
||||
setSelectedItem(item)
|
||||
setShowModalEditItem(true)
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
@@ -192,8 +197,8 @@ export const Components: FC = (): ReactElement => {
|
||||
size="sm"
|
||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalDeleteItem(true);
|
||||
setSelectedItem(item)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
@@ -224,7 +229,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
|
||||
<ModalEditItem
|
||||
@@ -236,16 +241,14 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
handleEditItem={handleEdit}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={async () => { await handleDelete(); return true; }}
|
||||
handleDeleteItem={async () => { await handleDelete(); return true }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+26
-21
@@ -1,10 +1,11 @@
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||
import { ReactElement, useState } from "react"
|
||||
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { cn, For } from '@imphnen-frontend-service/utils'
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
|
||||
|
||||
const TABS = {
|
||||
MENTORING: 'Mentoring',
|
||||
@@ -12,30 +13,34 @@ const TABS = {
|
||||
} as const
|
||||
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 [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: sessionsData, isLoading } = useMySessions(
|
||||
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
|
||||
);
|
||||
)
|
||||
|
||||
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.sessions ?? [])
|
||||
: [];
|
||||
: []
|
||||
const totalItems = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.total ?? sessions.length)
|
||||
: 0;
|
||||
: 0
|
||||
|
||||
const columns: ColumnDef<TSessionListItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn("w-20") },
|
||||
meta: { cellClassName: cn('w-20') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -76,17 +81,17 @@ export default function Components(): ReactElement {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const hasRating = !!row.original.rating;
|
||||
const hasRating = !!row.original.rating
|
||||
return (
|
||||
<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'}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
meta: { cellClassName: cn("w-72") },
|
||||
meta: { cellClassName: cn('w-72') },
|
||||
cell: () => (
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -116,7 +121,7 @@ export default function Components(): ReactElement {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="Dimentorin.dev">
|
||||
@@ -128,10 +133,10 @@ export default function Components(): ReactElement {
|
||||
<Button
|
||||
key={tab}
|
||||
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={() => {
|
||||
setActiveTab(tab);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
setActiveTab(tab)
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }))
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
@@ -175,5 +180,5 @@ export default function Components(): ReactElement {
|
||||
)}
|
||||
</section>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
+55
-54
@@ -1,15 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import * as React from 'react'
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -17,26 +16,30 @@ import {
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalUpdateItem from './_components/modal-update-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalAddItem from './_components/gacha-roll/modal-add-item'
|
||||
import ModalUpdateItem from './_components/gacha-roll/modal-update-item'
|
||||
import ModalDeleteItem from './_components/gacha-roll/modal-delete-item'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useGachaItemList,
|
||||
useCreateGachaItem,
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/gacha-roll')({
|
||||
component: GachaRollPage,
|
||||
})
|
||||
|
||||
function GachaRollPage() {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false)
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -47,30 +50,30 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
|
||||
const { data: itemsData, isLoading } = useGachaItemList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
})
|
||||
const createItem = useCreateGachaItem()
|
||||
const updateItem = useUpdateGachaItem()
|
||||
const deleteItem = useDeleteGachaItem()
|
||||
|
||||
const items: TGachaItemDto[] = itemsData?.data ?? [];
|
||||
const totalItems = itemsData?.meta?.total ?? items.length;
|
||||
const items: TGachaItemDto[] = itemsData?.data ?? []
|
||||
const totalItems = itemsData?.meta?.total ?? items.length
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
@@ -82,14 +85,14 @@ export const Components: FC = (): ReactElement => {
|
||||
weight: chanceRate ?? 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: {
|
||||
@@ -97,17 +100,17 @@ export const Components: FC = (): ReactElement => {
|
||||
weight: chanceRate,
|
||||
stock: quantity,
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
await deleteItem.mutateAsync(selectedItem.id)
|
||||
}
|
||||
setShowModalDeleteItem(false);
|
||||
};
|
||||
setShowModalDeleteItem(false)
|
||||
}
|
||||
|
||||
const columns: ColumnDef<TGachaItemDto>[] = [
|
||||
{
|
||||
@@ -145,9 +148,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedItem(row.original)
|
||||
setShowModalUpdateItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -157,9 +160,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedItem(row.original)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -168,7 +171,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: items,
|
||||
@@ -184,7 +187,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -235,7 +238,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalUpdateItem
|
||||
currentStep={currentStep}
|
||||
@@ -246,7 +249,7 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
handleUpdateItem={handleUpdate}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
@@ -254,7 +257,5 @@ export const Components: FC = (): ReactElement => {
|
||||
handleDeleteItem={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+18
-15
@@ -1,31 +1,36 @@
|
||||
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { FC, ReactElement } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
getAdminUsers,
|
||||
getAdminTeams,
|
||||
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({
|
||||
queryKey: ['admin-users-count'],
|
||||
queryFn: () => getAdminUsers({ page: 1, per_page: 1 }),
|
||||
});
|
||||
})
|
||||
|
||||
const { data: teamsData } = useQuery({
|
||||
queryKey: ['admin-teams-count'],
|
||||
queryFn: () => getAdminTeams({ page: 1, per_page: 1 }),
|
||||
});
|
||||
})
|
||||
|
||||
const { data: submissionsData } = useQuery({
|
||||
queryKey: ['admin-submissions-count'],
|
||||
queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }),
|
||||
});
|
||||
})
|
||||
|
||||
const totalParticipants = usersData?.meta?.total_data ?? '??';
|
||||
const totalTeams = teamsData?.meta?.total_data ?? '??';
|
||||
const totalSubmissions = submissionsData?.meta?.total_data ?? '??';
|
||||
const totalParticipants = usersData?.meta?.total_data ?? '??'
|
||||
const totalTeams = teamsData?.meta?.total_data ?? '??'
|
||||
const totalSubmissions = submissionsData?.meta?.total_data ?? '??'
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
||||
@@ -52,7 +57,5 @@ export const HackathonDashboardPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
</section>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default HackathonDashboardPage;
|
||||
)
|
||||
}
|
||||
+84
-73
@@ -1,3 +1,4 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
@@ -5,44 +6,52 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import SubmissionModal from './_components/submission-modal';
|
||||
} from 'react'
|
||||
import SubmissionModal from './_components/hackathon-submissions/submission-modal'
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
} from '@imphnen-frontend-service/ui/organisms';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
} from '@imphnen-frontend-service/ui/organisms'
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
LoadingOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
getAdminSubmissions,
|
||||
TAdminSubmissionItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
type SubmissionType = TAdminSubmissionItem;
|
||||
type SubmissionType = TAdminSubmissionItem
|
||||
|
||||
export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const currentPage = Math.max(
|
||||
1,
|
||||
parseInt(searchParams.get('page') || '1', 10)
|
||||
);
|
||||
const searchQuery = searchParams.get('search') || '';
|
||||
const perPage = parseInt(searchParams.get('per_page') || '10', 10);
|
||||
const statusFilter = searchParams.get('status') || 'all';
|
||||
export const Route = createFileRoute('/_authenticated/hackathon-submissions')({
|
||||
component: HackathonSubmissionsPage,
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
page: Number(search.page) || 1,
|
||||
search: (search.search as string) || '',
|
||||
per_page: Number(search.per_page) || 10,
|
||||
status: (search.status as string) || '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] =
|
||||
useState<SubmissionType | null>(null);
|
||||
const [globalFilter, setGlobalFilter] = useState(searchQuery);
|
||||
useState<SubmissionType | null>(null)
|
||||
const [globalFilter, setGlobalFilter] = useState(searchQuery)
|
||||
|
||||
const {
|
||||
data: submissionsResponse,
|
||||
@@ -65,82 +74,86 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
}),
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
const totalData = submissionsResponse?.meta?.total_data || 0;
|
||||
const totalPages = submissionsResponse?.meta?.total_page || 1;
|
||||
const totalData = submissionsResponse?.meta?.total_data || 0
|
||||
const totalPages = submissionsResponse?.meta?.total_page || 1
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', newPage.toString());
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
if (statusFilter !== 'all') params.set('status', statusFilter);
|
||||
setSearchParams(params);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
navigate({
|
||||
search: {
|
||||
page: newPage,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: searchQuery || undefined,
|
||||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||
} as any,
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
},
|
||||
[setSearchParams, perPage, searchQuery, statusFilter]
|
||||
);
|
||||
[navigate, perPage, searchQuery, statusFilter]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
setGlobalFilter(searchQuery)
|
||||
}, [searchQuery])
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (globalFilter.trim()) {
|
||||
params.set('search', globalFilter.trim());
|
||||
}
|
||||
if (statusFilter !== 'all') params.set('status', statusFilter);
|
||||
setSearchParams(params);
|
||||
}, [globalFilter, setSearchParams, perPage, statusFilter]);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: globalFilter.trim() || undefined,
|
||||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||
} as any,
|
||||
})
|
||||
}, [globalFilter, navigate, perPage, statusFilter])
|
||||
|
||||
const handleSearchKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
handleSearch()
|
||||
}
|
||||
},
|
||||
[handleSearch]
|
||||
);
|
||||
)
|
||||
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
params.set('per_page', newPerPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
if (statusFilter !== 'all') params.set('status', statusFilter);
|
||||
setSearchParams(params);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: newPerPage,
|
||||
search: searchQuery || undefined,
|
||||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||
} as any,
|
||||
})
|
||||
},
|
||||
[setSearchParams, searchQuery, statusFilter]
|
||||
);
|
||||
[navigate, searchQuery, statusFilter]
|
||||
)
|
||||
|
||||
const handleShowSubmissionModal = useCallback(
|
||||
(submission: SubmissionType) => {
|
||||
setSelectedSubmission(submission);
|
||||
setShowSubmissionModal(true);
|
||||
setSelectedSubmission(submission)
|
||||
setShowSubmissionModal(true)
|
||||
},
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
const handleCloseSubmissionModal = useCallback(() => {
|
||||
setShowSubmissionModal(false);
|
||||
setSelectedSubmission(null);
|
||||
}, []);
|
||||
setShowSubmissionModal(false)
|
||||
setSelectedSubmission(null)
|
||||
}, [])
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return submissionsResponse?.data || [];
|
||||
}, [submissionsResponse]);
|
||||
return submissionsResponse?.data || []
|
||||
}, [submissionsResponse])
|
||||
|
||||
const columns: ColumnDef<SubmissionType>[] = useMemo(
|
||||
() => [
|
||||
@@ -168,7 +181,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -182,7 +195,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
@@ -220,7 +233,7 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
[handleShowSubmissionModal]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
||||
@@ -308,7 +321,5 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
/>
|
||||
)}
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default HackathonSubmissionsPage;
|
||||
)
|
||||
}
|
||||
+93
-83
@@ -1,3 +1,4 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
@@ -5,16 +6,16 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import ModalTeamDetail from './_components/modal-team-detail-new';
|
||||
import { CityFilterSelect } from '../../../components/city-filter-select';
|
||||
} from 'react'
|
||||
import ModalTeamDetail from './_components/hackathon-teams/modal-team-detail-new'
|
||||
import { CityFilterSelect } from '../../components/city-filter-select'
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
} from '@imphnen-frontend-service/ui/organisms';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
} from '@imphnen-frontend-service/ui/organisms'
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
EditOutlined,
|
||||
TeamOutlined,
|
||||
@@ -22,32 +23,39 @@ import {
|
||||
FilterOutlined,
|
||||
PlusOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
getAdminTeams,
|
||||
TAdminTeamItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
type TeamType = TAdminTeamItem;
|
||||
type TeamType = TAdminTeamItem
|
||||
|
||||
export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const currentPage = Math.max(
|
||||
1,
|
||||
parseInt(searchParams.get('page') || '1', 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);
|
||||
export const Route = createFileRoute('/_authenticated/hackathon-teams')({
|
||||
component: HackathonTeamsPage,
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
page: Number(search.page) || 1,
|
||||
search: (search.search as string) || '',
|
||||
per_page: Number(search.per_page) || 10,
|
||||
}),
|
||||
})
|
||||
|
||||
const [visibilityFilter, setVisibilityFilter] = useState('all');
|
||||
const [cityFilter, setCityFilter] = useState('all');
|
||||
function HackathonTeamsPage() {
|
||||
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 {
|
||||
data: teamsResponse,
|
||||
@@ -70,84 +78,88 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
}),
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
const totalData = teamsResponse?.meta?.total_data || 0;
|
||||
const totalPages = teamsResponse?.meta?.total_page || 1;
|
||||
const totalData = teamsResponse?.meta?.total_data || 0
|
||||
const totalPages = teamsResponse?.meta?.total_page || 1
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', newPage.toString());
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
setSearchParams(params);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
navigate({
|
||||
search: {
|
||||
page: newPage,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: searchQuery || undefined,
|
||||
} as any,
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
},
|
||||
[setSearchParams, perPage, searchQuery]
|
||||
);
|
||||
[navigate, perPage, searchQuery]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
setGlobalFilter(searchQuery)
|
||||
}, [searchQuery])
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (globalFilter.trim()) {
|
||||
params.set('search', globalFilter.trim());
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [globalFilter, setSearchParams, perPage]);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: globalFilter.trim() || undefined,
|
||||
} as any,
|
||||
})
|
||||
}, [globalFilter, navigate, perPage])
|
||||
|
||||
const handleSearchKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
handleSearch()
|
||||
}
|
||||
},
|
||||
[handleSearch]
|
||||
);
|
||||
)
|
||||
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
params.set('per_page', newPerPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
setSearchParams(params);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: newPerPage,
|
||||
search: searchQuery || undefined,
|
||||
} as any,
|
||||
})
|
||||
},
|
||||
[setSearchParams, searchQuery]
|
||||
);
|
||||
[navigate, searchQuery]
|
||||
)
|
||||
|
||||
const handleShowDetailModal = useCallback((team: TeamType) => {
|
||||
setSelectedTeam(team);
|
||||
setShowDetailModal(true);
|
||||
}, []);
|
||||
setSelectedTeam(team)
|
||||
setShowDetailModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDetailModal = useCallback(() => {
|
||||
setShowDetailModal(false);
|
||||
setSelectedTeam(null);
|
||||
}, []);
|
||||
setShowDetailModal(false)
|
||||
setSelectedTeam(null)
|
||||
}, [])
|
||||
|
||||
const handleShowNewTeamModal = useCallback(() => {
|
||||
setShowNewTeamModal(true);
|
||||
}, []);
|
||||
setShowNewTeamModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseNewTeamModal = useCallback(() => {
|
||||
setShowNewTeamModal(false);
|
||||
}, []);
|
||||
setShowNewTeamModal(false)
|
||||
}, [])
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return teamsResponse?.data || [];
|
||||
}, [teamsResponse]);
|
||||
return teamsResponse?.data || []
|
||||
}, [teamsResponse])
|
||||
|
||||
const columns: ColumnDef<TeamType>[] = useMemo(
|
||||
() => [
|
||||
@@ -155,7 +167,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
accessorKey: 'name',
|
||||
header: 'Team',
|
||||
cell: ({ row }) => {
|
||||
const team = row.original;
|
||||
const team = row.original
|
||||
return (
|
||||
<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">
|
||||
@@ -178,7 +190,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
@@ -194,7 +206,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
accessorKey: 'visibility',
|
||||
header: 'Visibility',
|
||||
cell: ({ row }) => {
|
||||
const isPublic = row.original.visibility === 'public';
|
||||
const isPublic = row.original.visibility === 'public'
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -206,7 +218,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
>
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
@@ -256,7 +268,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
[handleShowDetailModal]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
||||
@@ -347,9 +359,9 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setVisibilityFilter('all');
|
||||
setCityFilter('all');
|
||||
setGlobalFilter('');
|
||||
setVisibilityFilter('all')
|
||||
setCityFilter('all')
|
||||
setGlobalFilter('')
|
||||
}}
|
||||
className="text-sm text-neutral-600"
|
||||
>
|
||||
@@ -401,7 +413,5 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
team={null}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default HackathonTeamsPage;
|
||||
)
|
||||
}
|
||||
+105
-95
@@ -1,3 +1,4 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
@@ -5,15 +6,15 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import ModalUserDetail from './_components/modal-user-detail';
|
||||
} from 'react'
|
||||
import ModalUserDetail from './_components/hackathon-users/modal-user-detail'
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
} from '@imphnen-frontend-service/ui/organisms';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
} from '@imphnen-frontend-service/ui/organisms'
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
EditOutlined,
|
||||
UserOutlined,
|
||||
@@ -21,16 +22,16 @@ import {
|
||||
FilterOutlined,
|
||||
PlusOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CityFilterSelect } from '../../../components/city-filter-select';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
} from '@ant-design/icons'
|
||||
import { CityFilterSelect } from '../../components/city-filter-select'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
getAdminUsers,
|
||||
TAdminUserItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import { useSearch, useNavigate } from '@tanstack/react-router'
|
||||
|
||||
type UserType = TAdminUserItem;
|
||||
type UserType = TAdminUserItem
|
||||
|
||||
const skillsOptions = [
|
||||
'Frontend Developer',
|
||||
@@ -41,24 +42,31 @@ const skillsOptions = [
|
||||
'Product Manager',
|
||||
'Data Scientist',
|
||||
'Mobile Developer',
|
||||
];
|
||||
]
|
||||
|
||||
export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const currentPage = Math.max(
|
||||
1,
|
||||
parseInt(searchParams.get('page') || '1', 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);
|
||||
export const Route = createFileRoute('/_authenticated/hackathon-users')({
|
||||
component: HackathonUsersPage,
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
page: Number(search.page) || 1,
|
||||
search: (search.search as string) || '',
|
||||
per_page: Number(search.per_page) || 10,
|
||||
}),
|
||||
})
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [cityFilter, setCityFilter] = useState('all');
|
||||
const [skillsFilter, setSkillsFilter] = useState<string[]>([]);
|
||||
function HackathonUsersPage() {
|
||||
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 [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 {
|
||||
data: usersResponse,
|
||||
@@ -81,100 +89,104 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
}),
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
const totalData = usersResponse?.meta?.total_data || 0;
|
||||
const totalPages = usersResponse?.meta?.total_page || 1;
|
||||
const totalData = usersResponse?.meta?.total_data || 0
|
||||
const totalPages = usersResponse?.meta?.total_page || 1
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', newPage.toString());
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
setSearchParams(params);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
navigate({
|
||||
search: {
|
||||
page: newPage,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: searchQuery || undefined,
|
||||
} as any,
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
},
|
||||
[setSearchParams, perPage, searchQuery]
|
||||
);
|
||||
[navigate, perPage, searchQuery]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
setGlobalFilter(searchQuery)
|
||||
}, [searchQuery])
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
if (globalFilter.trim()) {
|
||||
params.set('search', globalFilter.trim());
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [globalFilter, setSearchParams, perPage]);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: perPage !== 10 ? perPage : undefined,
|
||||
search: globalFilter.trim() || undefined,
|
||||
} as any,
|
||||
})
|
||||
}, [globalFilter, navigate, perPage])
|
||||
|
||||
const handleSearchKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
handleSearch()
|
||||
}
|
||||
},
|
||||
[handleSearch]
|
||||
);
|
||||
)
|
||||
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
params.set('per_page', newPerPage.toString());
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
setSearchParams(params);
|
||||
navigate({
|
||||
search: {
|
||||
page: 1,
|
||||
per_page: newPerPage,
|
||||
search: searchQuery || undefined,
|
||||
} as any,
|
||||
})
|
||||
},
|
||||
[setSearchParams, searchQuery]
|
||||
);
|
||||
[navigate, searchQuery]
|
||||
)
|
||||
|
||||
const handleShowDetailModal = useCallback((user: UserType) => {
|
||||
setSelectedUser(user);
|
||||
setShowDetailModal(true);
|
||||
}, []);
|
||||
setSelectedUser(user)
|
||||
setShowDetailModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDetailModal = useCallback(() => {
|
||||
setShowDetailModal(false);
|
||||
setSelectedUser(null);
|
||||
}, []);
|
||||
setShowDetailModal(false)
|
||||
setSelectedUser(null)
|
||||
}, [])
|
||||
|
||||
const handleShowNewUserModal = useCallback(() => {
|
||||
setShowNewUserModal(true);
|
||||
}, []);
|
||||
setShowNewUserModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseNewUserModal = useCallback(() => {
|
||||
setShowNewUserModal(false);
|
||||
}, []);
|
||||
setShowNewUserModal(false)
|
||||
}, [])
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
const usersData = usersResponse?.data || [];
|
||||
const usersData = usersResponse?.data || []
|
||||
return usersData.filter((user: UserType) => {
|
||||
if (statusFilter !== 'all') {
|
||||
const isActive = statusFilter === 'active';
|
||||
if (user.is_active !== isActive) return false;
|
||||
const isActive = statusFilter === 'active'
|
||||
if (user.is_active !== isActive) return false
|
||||
}
|
||||
|
||||
if (skillsFilter.length > 0) {
|
||||
const userSkills = user.skills || [];
|
||||
const userSkills = user.skills || []
|
||||
const hasMatchingSkill = skillsFilter.some((skill) =>
|
||||
userSkills.includes(skill)
|
||||
);
|
||||
if (!hasMatchingSkill) return false;
|
||||
)
|
||||
if (!hasMatchingSkill) return false
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [usersResponse, statusFilter, skillsFilter]);
|
||||
return true
|
||||
})
|
||||
}, [usersResponse, statusFilter, skillsFilter])
|
||||
|
||||
const columns: ColumnDef<UserType>[] = useMemo(
|
||||
() => [
|
||||
@@ -207,7 +219,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
accessorKey: 'skills',
|
||||
header: 'Skills',
|
||||
cell: ({ row }) => {
|
||||
const skills = row.original.skills || [];
|
||||
const skills = row.original.skills || []
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{skills.length > 0 ? (
|
||||
@@ -230,7 +242,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<span className="text-neutral-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -265,11 +277,11 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
),
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const aActive = rowA.original.is_active;
|
||||
const bActive = rowB.original.is_active;
|
||||
if (aActive && !bActive) return -1;
|
||||
if (!aActive && bActive) return 1;
|
||||
return 0;
|
||||
const aActive = rowA.original.is_active
|
||||
const bActive = rowB.original.is_active
|
||||
if (aActive && !bActive) return -1
|
||||
if (!aActive && bActive) return 1
|
||||
return 0
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,7 +322,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
[handleShowDetailModal]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
||||
@@ -424,10 +436,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setStatusFilter('all');
|
||||
setCityFilter('all');
|
||||
setSkillsFilter([]);
|
||||
setGlobalFilter('');
|
||||
setStatusFilter('all')
|
||||
setCityFilter('all')
|
||||
setSkillsFilter([])
|
||||
setGlobalFilter('')
|
||||
}}
|
||||
className="text-sm text-neutral-600"
|
||||
>
|
||||
@@ -479,7 +491,5 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
user={null}
|
||||
/>
|
||||
</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 {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -14,27 +15,31 @@ import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddPermission from './_components/modal-add-permission';
|
||||
import ModalUpdatePermission from './_components/modal-update-permission';
|
||||
import ModalDeletePermission from './_components/modal-delete-permission';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalAddPermission from './_components/permissions/modal-add-permission'
|
||||
import ModalUpdatePermission from './_components/permissions/modal-update-permission'
|
||||
import ModalDeletePermission from './_components/permissions/modal-delete-permission'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
usePermissionList,
|
||||
useCreatePermission,
|
||||
useUpdatePermission,
|
||||
useDeletePermission,
|
||||
TPermissionItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import React from 'react';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import React from 'react'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/permissions')({
|
||||
component: PermissionsPage,
|
||||
})
|
||||
|
||||
function PermissionsPage() {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false)
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
|
||||
const { data: permissionsData, isLoading } = usePermissionList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createPermission = useCreatePermission();
|
||||
const updatePermission = useUpdatePermission();
|
||||
const deletePermission = useDeletePermission();
|
||||
})
|
||||
const createPermission = useCreatePermission()
|
||||
const updatePermission = useUpdatePermission()
|
||||
const deletePermission = useDeletePermission()
|
||||
|
||||
const permissions: TPermissionItem[] = permissionsData?.data ?? [];
|
||||
const totalItems = permissionsData?.meta?.total ?? permissions.length;
|
||||
const permissions: TPermissionItem[] = permissionsData?.data ?? []
|
||||
const totalItems = permissionsData?.meta?.total ?? permissions.length
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
await createPermission.mutateAsync(pendingFormData.current);
|
||||
await createPermission.mutateAsync(pendingFormData.current)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
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> => {
|
||||
if (selectedItem) {
|
||||
await deletePermission.mutateAsync(selectedItem.id);
|
||||
await deletePermission.mutateAsync(selectedItem.id)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const columns: ColumnDef<TPermissionItem>[] = [
|
||||
{
|
||||
@@ -123,9 +128,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedItem(row.original)
|
||||
setShowModalUpdateItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -135,9 +140,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedItem(row.original)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -146,7 +151,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: permissions,
|
||||
@@ -162,7 +167,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -218,7 +223,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalUpdatePermission
|
||||
isOpen={showModalUpdateItem}
|
||||
@@ -228,7 +233,7 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
handleUpdate={handleUpdate}
|
||||
initialValues={selectedItem ? { name: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalDeletePermission
|
||||
isOpen={showModalDeleteItem}
|
||||
@@ -239,7 +244,5 @@ export const Components: FC = (): ReactElement => {
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+44
-43
@@ -1,14 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import * as React from 'react'
|
||||
import { FC, Fragment, ReactElement, useState } from 'react'
|
||||
import {
|
||||
FilterOutlined,
|
||||
SearchOutlined,
|
||||
AuditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -16,19 +15,19 @@ import {
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalProcessDelivery from './_components/modal-process-item';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalProcessDelivery from './_components/prizes/modal-process-item'
|
||||
|
||||
type OrderValid = 'valid' | 'invalid' | 'unchecked';
|
||||
type Status = 'undelivered' | 'delivered';
|
||||
type OrderValid = 'valid' | 'invalid' | 'unchecked'
|
||||
type Status = 'undelivered' | 'delivered'
|
||||
|
||||
interface Prize {
|
||||
id: number;
|
||||
name: string;
|
||||
orderValid: OrderValid;
|
||||
items: string;
|
||||
address: string;
|
||||
status: Status;
|
||||
id: number
|
||||
name: string
|
||||
orderValid: OrderValid
|
||||
items: string
|
||||
address: string
|
||||
status: Status
|
||||
}
|
||||
|
||||
const items = [
|
||||
@@ -38,7 +37,7 @@ const items = [
|
||||
'Sticker Isi 3',
|
||||
'Sticker Isi 5',
|
||||
'Gelang Karet',
|
||||
];
|
||||
]
|
||||
|
||||
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
@@ -51,24 +50,28 @@ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
items: items[i % items.length],
|
||||
address: 'Jl. Pantai Cibaduyut Indah',
|
||||
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] =
|
||||
useState(false);
|
||||
useState(false)
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
|
||||
const deliveryOptions = [
|
||||
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
|
||||
{ id: 'option1', value: 'delivered', label: 'Delivered' },
|
||||
];
|
||||
]
|
||||
|
||||
const columns: ColumnDef<Prize>[] = [
|
||||
{
|
||||
@@ -102,24 +105,24 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Order Valid?',
|
||||
accessorKey: 'orderValid',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.orderValid;
|
||||
const status = row.original.orderValid
|
||||
const statusColors: Record<OrderValid, string> = {
|
||||
valid: 'bg-success-200 text-success-500',
|
||||
invalid: 'bg-danger-200 text-danger-500',
|
||||
unchecked: 'bg-warning-200 text-warning-900',
|
||||
};
|
||||
}
|
||||
const statusText: Record<OrderValid, string> = {
|
||||
valid: 'Valid',
|
||||
invalid: 'Invalid',
|
||||
unchecked: 'Unchecked',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -134,22 +137,22 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
const statusColors: Record<Status, string> = {
|
||||
delivered: 'bg-success-200 text-success-500',
|
||||
undelivered: 'bg-danger-200 text-danger-500',
|
||||
};
|
||||
}
|
||||
const statusText: Record<Status, string> = {
|
||||
delivered: 'Delivered',
|
||||
undelivered: 'Undelivered',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -159,8 +162,8 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalProcessDelivery(true);
|
||||
e.stopPropagation()
|
||||
setShowModalProcessDelivery(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-full"
|
||||
>
|
||||
@@ -168,7 +171,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
@@ -184,7 +187,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -219,7 +222,7 @@ export const Components: FC = (): ReactElement => {
|
||||
options={deliveryOptions}
|
||||
onClose={() => setShowFilter(false)}
|
||||
onFilterChange={(value) => {
|
||||
console.log('Selected filter:', value);
|
||||
console.log('Selected filter:', value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -234,11 +237,9 @@ export const Components: FC = (): ReactElement => {
|
||||
isOpen={showModalProcessDelivery}
|
||||
onClose={() => setShowModalProcessDelivery(false)}
|
||||
handleProcessDelivery={() => {
|
||||
console.log('Action ketika user menekan tombol Proses Pengiriman');
|
||||
console.log('Action ketika user menekan tombol Proses Pengiriman')
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+23
-18
@@ -1,10 +1,11 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { cn } from "@imphnen-frontend-service/utils";
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
|
||||
import { useState } from 'react'
|
||||
import { ModalCreateRoadmap } from './_components/roadmap-dimentorin/modal/create-roadmap'
|
||||
|
||||
type LearningStatus = 'active' | 'inactive'
|
||||
|
||||
@@ -22,19 +23,23 @@ const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
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 [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const columns: ColumnDef<RoadmapType>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn("w-20") },
|
||||
meta: { cellClassName: cn('w-20') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -72,34 +77,34 @@ export default function Components(): React.ReactElement {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
const statusColors: Record<LearningStatus, string> = {
|
||||
inactive: 'bg-danger-200 text-danger-700',
|
||||
active: 'bg-success-200 text-success-500',
|
||||
};
|
||||
}
|
||||
const statusText: Record<LearningStatus, string> = {
|
||||
inactive: 'Inactive',
|
||||
active: 'Active',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
meta: { cellClassName: cn("w-72") },
|
||||
meta: { cellClassName: cn('w-72') },
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -109,7 +114,7 @@ export default function Components(): React.ReactElement {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -134,7 +139,7 @@ export default function Components(): React.ReactElement {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<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 {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -14,27 +15,31 @@ import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddRole from './_components/modal-add-role';
|
||||
import ModalUpdateRole from './_components/modal-update-role';
|
||||
import ModalDeleteRole from './_components/modal-delete-role';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalAddRole from './_components/roles/modal-add-role'
|
||||
import ModalUpdateRole from './_components/roles/modal-update-role'
|
||||
import ModalDeleteRole from './_components/roles/modal-delete-role'
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
useRoleList,
|
||||
useCreateRole,
|
||||
useUpdateRole,
|
||||
useDeleteRole,
|
||||
TRolesListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import React from 'react';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import React from 'react'
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
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);
|
||||
export const Route = createFileRoute('/_authenticated/roles')({
|
||||
component: RolesPage,
|
||||
})
|
||||
|
||||
function RolesPage() {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false)
|
||||
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 {
|
||||
step: currentStep,
|
||||
@@ -45,47 +50,47 @@ export const Components: FC = (): ReactElement => {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
})
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
|
||||
const { data: rolesData, isLoading } = useRoleList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createRole = useCreateRole();
|
||||
const updateRole = useUpdateRole();
|
||||
const deleteRole = useDeleteRole();
|
||||
})
|
||||
const createRole = useCreateRole()
|
||||
const updateRole = useUpdateRole()
|
||||
const deleteRole = useDeleteRole()
|
||||
|
||||
const roles: TRolesListItem[] = rolesData?.data ?? [];
|
||||
const totalItems = rolesData?.meta?.total ?? roles.length;
|
||||
const roles: TRolesListItem[] = rolesData?.data ?? []
|
||||
const totalItems = rolesData?.meta?.total ?? roles.length
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
await createRole.mutateAsync(pendingFormData.current);
|
||||
await createRole.mutateAsync(pendingFormData.current)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
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> => {
|
||||
if (selectedRole) {
|
||||
await deleteRole.mutateAsync(selectedRole.id);
|
||||
await deleteRole.mutateAsync(selectedRole.id)
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const columns: ColumnDef<TRolesListItem>[] = [
|
||||
{
|
||||
@@ -123,9 +128,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedRole(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedRole(row.original)
|
||||
setShowModalUpdateItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -135,9 +140,9 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedRole(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
e.stopPropagation()
|
||||
setSelectedRole(row.original)
|
||||
setShowModalDeleteItem(true)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -146,7 +151,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: roles,
|
||||
@@ -162,7 +167,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -218,7 +223,7 @@ export const Components: FC = (): ReactElement => {
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAdd={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalUpdateRole
|
||||
isOpen={showModalUpdateItem}
|
||||
@@ -228,7 +233,7 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
handleUpdate={handleUpdate}
|
||||
initialValues={selectedRole ? { name: selectedRole.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
onDataCapture={(data) => { pendingFormData.current = data }}
|
||||
/>
|
||||
<ModalDeleteRole
|
||||
isOpen={showModalDeleteItem}
|
||||
@@ -239,7 +244,5 @@ export const Components: FC = (): ReactElement => {
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+28
-23
@@ -1,33 +1,38 @@
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { cn } from "@imphnen-frontend-service/utils";
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||
import { ReactElement, useState } from "react";
|
||||
import { ModalDetailSession } from "./_components/modal/detail";
|
||||
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { ModalDetailSession } from './_components/session-dimentorin/modal/detail'
|
||||
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const [openDetail, setOpenDetail] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
export const Route = createFileRoute('/_authenticated/session-dimentorin')({
|
||||
component: SessionDimentorinPage,
|
||||
})
|
||||
|
||||
function SessionDimentorinPage(): ReactElement {
|
||||
const [openDetail, setOpenDetail] = useState(false)
|
||||
const [statusFilter, setStatusFilter] = useState('')
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: sessionsData, isLoading } = useMySessions(
|
||||
statusFilter ? { status: statusFilter } : undefined
|
||||
);
|
||||
)
|
||||
|
||||
const sessions: TSessionListItem[] = sessionsData?.sessions ?? [];
|
||||
const totalItems = sessionsData?.total ?? sessions.length;
|
||||
const sessions: TSessionListItem[] = sessionsData?.sessions ?? []
|
||||
const totalItems = sessionsData?.total ?? sessions.length
|
||||
|
||||
const columns: ColumnDef<TSessionListItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn("w-20") },
|
||||
meta: { cellClassName: cn('w-20') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -73,31 +78,31 @@ export default function Components(): ReactElement {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'bg-warning-200 text-warning-700',
|
||||
confirmed: 'bg-primary-200 text-primary-700',
|
||||
ongoing: 'bg-warning-200 text-warning-700',
|
||||
completed: 'bg-success-200 text-success-500',
|
||||
cancelled: 'bg-danger-200 text-danger-500',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
|
||||
{status}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
meta: { cellClassName: cn("w-52") },
|
||||
meta: { cellClassName: cn('w-52') },
|
||||
cell: () => (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenDetail(true);
|
||||
e.stopPropagation()
|
||||
setOpenDetail(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-max"
|
||||
>
|
||||
@@ -121,7 +126,7 @@ export default function Components(): ReactElement {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="Dimentorin.dev">
|
||||
+21
-16
@@ -1,22 +1,27 @@
|
||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
|
||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||
import { useState } from "react"
|
||||
import { GeneralSettings } from "./_components/general"
|
||||
import { UserRolesPermission } from "./_components/user-roles-permission"
|
||||
import { NotificationSettings } from "./_components/notification"
|
||||
import { SecuritySettings } from "./_components/security"
|
||||
import { PaymentSettings } from "./_components/payment"
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { cn, For } from '@imphnen-frontend-service/utils'
|
||||
import { useState } from 'react'
|
||||
import { GeneralSettings } from './_components/settings-dimentorin/general'
|
||||
import { UserRolesPermission } from './_components/settings-dimentorin/user-roles-permission'
|
||||
import { NotificationSettings } from './_components/settings-dimentorin/notification'
|
||||
import { SecuritySettings } from './_components/settings-dimentorin/security'
|
||||
import { PaymentSettings } from './_components/settings-dimentorin/payment'
|
||||
|
||||
const TABS = {
|
||||
general: "General Settings",
|
||||
userRolePermissions: "User Roles & Permissions",
|
||||
notification: "Notification Settings",
|
||||
security: "Security",
|
||||
payment: "Payment",
|
||||
general: 'General Settings',
|
||||
userRolePermissions: 'User Roles & Permissions',
|
||||
notification: 'Notification Settings',
|
||||
security: 'Security',
|
||||
payment: 'Payment',
|
||||
} as const
|
||||
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)
|
||||
|
||||
return (
|
||||
@@ -30,8 +35,8 @@ export default function Components(): React.ReactElement {
|
||||
<button
|
||||
key={tab}
|
||||
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",
|
||||
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
|
||||
'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'
|
||||
)}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
+37
-35
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import * as React from 'react'
|
||||
import { FC, Fragment, ReactElement, useState } from 'react'
|
||||
import {
|
||||
FilterOutlined,
|
||||
SearchOutlined,
|
||||
AuditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
} from '@ant-design/icons'
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -15,16 +15,16 @@ import {
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalValidate from './_components/modal-validate';
|
||||
} from '@tanstack/react-table'
|
||||
import ModalValidate from './_components/transactions/modal-validate'
|
||||
|
||||
type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
|
||||
type TransactionStatus = 'valid' | 'invalid' | 'unchecked'
|
||||
|
||||
interface Transaction {
|
||||
id: number;
|
||||
name: string;
|
||||
transactionNumber: string;
|
||||
status: TransactionStatus;
|
||||
id: number
|
||||
name: string
|
||||
transactionNumber: string
|
||||
status: TransactionStatus
|
||||
}
|
||||
|
||||
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
||||
@@ -36,24 +36,28 @@ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
||||
: i % 5 === 0
|
||||
? 'unchecked'
|
||||
: 'valid') as TransactionStatus,
|
||||
}));
|
||||
}))
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalValidate, setShowModalValidate] = useState(false);
|
||||
export const Route = createFileRoute('/_authenticated/transactions')({
|
||||
component: TransactionsPage,
|
||||
})
|
||||
|
||||
function TransactionsPage() {
|
||||
const [showModalValidate, setShowModalValidate] = useState(false)
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
|
||||
const validationOptions = [
|
||||
{ id: 'option1', value: 'unchecked', label: 'Unchecked' },
|
||||
{ id: 'option2', value: 'valid', label: 'Valid' },
|
||||
{ id: 'option3', value: 'invalid', label: 'Invalid' },
|
||||
];
|
||||
]
|
||||
|
||||
const columns: ColumnDef<Transaction>[] = [
|
||||
{
|
||||
@@ -91,24 +95,24 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Order Valid?',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
const statusColors: Record<TransactionStatus, string> = {
|
||||
valid: 'bg-success-200 text-success-500',
|
||||
invalid: 'bg-danger-200 text-danger-500',
|
||||
unchecked: 'bg-warning-200 text-warning-900',
|
||||
};
|
||||
}
|
||||
const statusText: Record<TransactionStatus, string> = {
|
||||
valid: 'Valid',
|
||||
invalid: 'Invalid',
|
||||
unchecked: 'Unchecked',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -118,8 +122,8 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalValidate(true);
|
||||
e.stopPropagation()
|
||||
setShowModalValidate(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-full"
|
||||
>
|
||||
@@ -127,7 +131,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockTransactions,
|
||||
@@ -143,7 +147,7 @@ export const Components: FC = (): ReactElement => {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -180,7 +184,7 @@ export const Components: FC = (): ReactElement => {
|
||||
options={validationOptions}
|
||||
onClose={() => setShowFilter(false)}
|
||||
onFilterChange={(value) => {
|
||||
console.log('Selected filter:', value);
|
||||
console.log('Selected filter:', value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -195,14 +199,12 @@ export const Components: FC = (): ReactElement => {
|
||||
isOpen={showModalValidate}
|
||||
onClose={() => setShowModalValidate(false)}
|
||||
handleValid={() => {
|
||||
console.log('Action ketika user klik Valid');
|
||||
console.log('Action ketika user klik Valid')
|
||||
}}
|
||||
handleInvalid={() => {
|
||||
console.log('Action ketika user klik Tidak Valid');
|
||||
console.log('Action ketika user klik Tidak Valid')
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
)
|
||||
}
|
||||
+45
-40
@@ -1,10 +1,11 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
} from '@imphnen-frontend-service/ui/organisms';
|
||||
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||
} from '@imphnen-frontend-service/ui/organisms'
|
||||
import { cn, For } from '@imphnen-frontend-service/utils'
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
@@ -12,48 +13,52 @@ import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { ReactElement, useState } from 'react';
|
||||
import { ModalDetailUser } from './_components/modal/detail';
|
||||
} from '@tanstack/react-table'
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { ModalDetailUser } from './_components/users-dimentorin/modal/detail'
|
||||
import {
|
||||
useMentorList,
|
||||
useUserList,
|
||||
MentorDetailResponseDto,
|
||||
TUsersListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
|
||||
export default function Components(): 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('');
|
||||
export const Route = createFileRoute('/_authenticated/users-dimentorin')({
|
||||
component: UsersDimentorinPage,
|
||||
})
|
||||
|
||||
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>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: mentorData, isLoading: mentorLoading } = useMentorList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: menteeData, isLoading: menteeLoading } = useUserList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
})
|
||||
|
||||
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? [];
|
||||
const mentees: TUsersListItem[] = menteeData?.data ?? [];
|
||||
const mentorTotal = mentorData?.meta?.total ?? mentors.length;
|
||||
const menteeTotal = menteeData?.meta?.total ?? mentees.length;
|
||||
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? []
|
||||
const mentees: TUsersListItem[] = menteeData?.data ?? []
|
||||
const mentorTotal = mentorData?.meta?.total ?? mentors.length
|
||||
const menteeTotal = menteeData?.meta?.total ?? mentees.length
|
||||
|
||||
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading;
|
||||
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal;
|
||||
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading
|
||||
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal
|
||||
|
||||
const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [
|
||||
{
|
||||
@@ -97,17 +102,17 @@ export default function Components(): ReactElement {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const status = row.original.status
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'bg-success-200 text-success-500',
|
||||
pending: 'bg-warning-200 text-warning-700',
|
||||
inactive: 'bg-danger-200 text-danger-500',
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
|
||||
{status}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -118,9 +123,9 @@ export default function Components(): ReactElement {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedUserId(row.original.id);
|
||||
setShowDetail(true);
|
||||
e.stopPropagation()
|
||||
setSelectedUserId(row.original.id)
|
||||
setShowDetail(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-max"
|
||||
>
|
||||
@@ -128,7 +133,7 @@ export default function Components(): ReactElement {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const menteeColumns: ColumnDef<TUsersListItem>[] = [
|
||||
{
|
||||
@@ -179,9 +184,9 @@ export default function Components(): ReactElement {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedUserId(row.original.id);
|
||||
setShowDetail(true);
|
||||
e.stopPropagation()
|
||||
setSelectedUserId(row.original.id)
|
||||
setShowDetail(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-max"
|
||||
>
|
||||
@@ -189,7 +194,7 @@ export default function Components(): ReactElement {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const mentorTable = useReactTable({
|
||||
data: mentors,
|
||||
@@ -202,7 +207,7 @@ export default function Components(): ReactElement {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mentorTotal / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
const menteeTable = useReactTable({
|
||||
data: mentees,
|
||||
@@ -215,7 +220,7 @@ export default function Components(): ReactElement {
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(menteeTotal / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="Dimentorin.dev">
|
||||
@@ -234,8 +239,8 @@ export default function Components(): ReactElement {
|
||||
activeTab === tab && 'bg-white'
|
||||
)}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
setActiveTab(tab)
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }))
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
@@ -275,5 +280,5 @@ export default function Components(): ReactElement {
|
||||
userId={selectedUserId}
|
||||
/>
|
||||
</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,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useLogin = () => {
|
||||
@@ -25,7 +25,7 @@ export const useLogin = () => {
|
||||
try {
|
||||
await loginMutation.mutateAsync(data);
|
||||
toast.success('Login berhasil!');
|
||||
navigate('/hackathon-dashboard');
|
||||
navigate({ to: '/hackathon-dashboard' });
|
||||
} catch (error) {
|
||||
console.error('[Backoffice Login] Error:', error);
|
||||
toast.error((error as Error).message || 'Login gagal');
|
||||
+25
-20
@@ -1,33 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { useLogin, authLoginSchema, TLoginRequest } from '@imphnen-frontend-service/service';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useLogin, authLoginSchema, TLoginRequest } from '@imphnen-frontend-service/service'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { toast } from 'sonner'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const loginMutation = useLogin();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
export const Route = createFileRoute('/_public/auth/login')({
|
||||
component: LoginPage,
|
||||
})
|
||||
|
||||
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>({
|
||||
resolver: zodResolver(authLoginSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: { email: '', password: '' },
|
||||
});
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setError(null);
|
||||
setError(null)
|
||||
try {
|
||||
await loginMutation.mutateAsync(data);
|
||||
toast.success('Login successful!');
|
||||
navigate('/');
|
||||
await loginMutation.mutateAsync(data)
|
||||
toast.success('Login successful!')
|
||||
navigate({ to: '/' })
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Login failed');
|
||||
setError((err as Error).message || 'Login failed')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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 { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
@@ -15,7 +16,14 @@ export default defineConfig(() => ({
|
||||
port: 3001,
|
||||
host: 'localhost',
|
||||
},
|
||||
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
plugins: [
|
||||
TanStackRouterVite({
|
||||
routeFileIgnorePattern: '_components|_hooks|_hook|_data',
|
||||
}),
|
||||
react(),
|
||||
nxViteTsPaths(),
|
||||
nxCopyAssetsPlugin(['*.md']),
|
||||
],
|
||||
build: {
|
||||
outDir: '../../dist/apps/backoffice',
|
||||
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 { middleware } from './middleware';
|
||||
import { StrictMode } from 'react';
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
||||
import {
|
||||
add404PageToRoutesChildren,
|
||||
addErrorElementToRoutes,
|
||||
convertPagesToRoute,
|
||||
ModalLoginProvider,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { ModalLoginProvider, QueryProvider } from '@imphnen-frontend-service/utils'
|
||||
import { Toaster } from 'sonner'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import './index.css'
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
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 router = createRouter({ routeTree })
|
||||
|
||||
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
||||
addErrorElementToRoutes(errorFiles, routes);
|
||||
add404PageToRoutesChildren(notFoundFiles, routes);
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
...routes,
|
||||
loader: middleware,
|
||||
shouldRevalidate: () => true,
|
||||
},
|
||||
]);
|
||||
const rootElement = document.getElementById('root')
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
@@ -42,5 +27,4 @@ createRoot(rootElement).render(
|
||||
</ModalLoginProvider>
|
||||
</QueryProvider>
|
||||
</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