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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-11 17:04:14 +07:00
co-authored by Claude Opus 4.6
parent 86d9e759a6
commit 4240e8eb51
291 changed files with 3889 additions and 4585 deletions
@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
-23
View File
@@ -1,23 +0,0 @@
import { Link } from 'react-router-dom';
export default function NotFoundPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
The page you are looking for doesn't exist or has been moved.
</p>
<Link
to="/hackathon-dashboard"
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Go Back Home
</Link>
</div>
</div>
);
}
-29
View File
@@ -1,29 +0,0 @@
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
export default function ErrorPage() {
const error = useRouteError();
let errorMessage: string;
if (isRouteErrorResponse(error)) {
errorMessage = error.statusText;
} else if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
} else {
console.error(error);
errorMessage = 'Unknown error';
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
<p className="text-xl text-gray-700 mb-2">
Sorry, an unexpected error has occurred.
</p>
<p className="text-gray-500 italic">{errorMessage}</p>
</div>
</div>
);
}
+17 -31
View File
@@ -1,42 +1,28 @@
import { createRoot } from 'react-dom/client';
import { 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>
);
)
-100
View File
@@ -1,100 +0,0 @@
import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/hackathon-dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/hackathon-dashboard';
}
}
return null;
};
+2
View File
@@ -0,0 +1,2 @@
// This file is auto-generated by TanStack Router
export const routeTree = {} as any
+5
View File
@@ -0,0 +1,5 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => <Outlet />,
})
@@ -1,9 +1,20 @@
import { FC, ReactElement, useState } from 'react';
import { 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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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>
@@ -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;
)
}
@@ -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>
);
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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;
)
}
@@ -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">
@@ -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;
)
}
@@ -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">
@@ -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)}
>
@@ -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;
)
}
@@ -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>
);
)
}
+20
View File
@@ -0,0 +1,20 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_public')({
beforeLoad: () => {
const session = SessionToken.get()
if (session?.token?.access_token) {
throw redirect({ to: '/hackathon-dashboard' })
}
},
component: PublicLayout,
})
function PublicLayout() {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
)
}
@@ -5,7 +5,7 @@ import {
useBackofficeLogin,
} 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');
@@ -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>
);
)
}
+12
View File
@@ -0,0 +1,12 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/')({
beforeLoad: () => {
const session = SessionToken.get()
if (session?.token?.access_token) {
throw redirect({ to: '/hackathon-dashboard' })
}
throw redirect({ to: '/auth/login' })
},
})
+9 -1
View File
@@ -3,6 +3,7 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { 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;
-10
View File
@@ -1,10 +0,0 @@
import { Outlet, ScrollRestoration } from "react-router-dom";
export default function RootLayout() {
return (
<>
<Outlet />
<ScrollRestoration />
</>
)
}
+16 -32
View File
@@ -1,37 +1,22 @@
import { createRoot } from 'react-dom/client';
import { 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
)
-118
View File
@@ -1,118 +0,0 @@
import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/forgot/otp',
'/auth/register',
'/auth/register/otp',
'/auth/register/success',
'/auth/new-password',
'/auth/register-mentor',
'/auth/register-mentor/pending',
'/auth/register-mentor/success',
'/auth/google-callback',
'/auth/google-oauth-popup',
'/resources',
];const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
const mappingPublicPrefixRoutes = [
'/mentoring',
'/articles',
]
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
return null;
}
if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null;
};
+25
View File
@@ -0,0 +1,25 @@
/* prettier-ignore-start */
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file is auto-generated by TanStack Router
// Import Routes
import { Route as rootRoute } from './routes/__root'
// Create/Update Routes
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {}
}
// Create and export the route tree
export const routeTree = rootRoute
+10
View File
@@ -0,0 +1,10 @@
import { createRootRoute, Outlet, ScrollRestoration } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<ScrollRestoration />
</>
),
})
@@ -0,0 +1,16 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { SessionToken } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: () => {
const session = SessionToken.get()
if (!session?.token?.access_token) {
throw redirect({ to: '/auth/login' })
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
return <Outlet />
}

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