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:
maulanasdqnandClaude Opus 4.6 committed 2026-04-11 17:04:14 +07:00
1 parent 86d9e759a6
commit 4240e8eb51
291 files changed
+3889 -4585

No files matched your search

@@ -1,12 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
-23
View File
@@ -1,23 +0,0 @@
import { Link } from 'react-router-dom';
export default function NotFoundPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
The page you are looking for doesn't exist or has been moved.
</p>
<Link
to="/hackathon-dashboard"
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Go Back Home
</Link>
</div>
</div>
);
}
-29
View File
@@ -1,29 +0,0 @@
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
export default function ErrorPage() {
const error = useRouteError();
let errorMessage: string;
if (isRouteErrorResponse(error)) {
errorMessage = error.statusText;
} else if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
} else {
console.error(error);
errorMessage = 'Unknown error';
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
<p className="text-xl text-gray-700 mb-2">
Sorry, an unexpected error has occurred.
</p>
<p className="text-gray-500 italic">{errorMessage}</p>
</div>
</div>
);
}
+17 -31
View File
@@ -1,42 +1,28 @@
import { createRoot } from 'react-dom/client'; import { StrictMode } from 'react'
import { middleware } from './middleware'; import { createRoot } from 'react-dom/client'
import { StrictMode } from 'react'; import { RouterProvider, createRouter } from '@tanstack/react-router'
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router'; import { QueryProvider } from '@imphnen-frontend-service/utils'
import { import { Toaster } from 'sonner'
add404PageToRoutesChildren, import { routeTree } from './routeTree.gen'
addErrorElementToRoutes, import './index.css'
convertPagesToRoute,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const router = createRouter({ routeTree })
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject; declare module '@tanstack/react-router' {
addErrorElementToRoutes(errorFiles, routes); interface Register {
add404PageToRoutesChildren(notFoundFiles, routes); router: typeof router
}
}
const router = createBrowserRouter([ const rootElement = document.getElementById('root')
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element')
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <QueryProvider>
<Toaster position="top-right" />
<RouterProvider router={router} /> <RouterProvider router={router} />
<Toaster position="top-right" />
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
); )
-100
View File
@@ -1,100 +0,0 @@
import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/hackathon-dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/hackathon-dashboard';
}
}
return null;
};
+2
View File
@@ -0,0 +1,2 @@
// This file is auto-generated by TanStack Router
export const routeTree = {} as any
+5
View File
@@ -0,0 +1,5 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => <Outlet />,
})
@@ -1,9 +1,20 @@
import { FC, ReactElement, useState } from 'react'; import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { Outlet } from 'react-router-dom'; import { SessionToken } from '@imphnen-frontend-service/service'
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; import { useState } from 'react'
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'
export const AppLayout: FC = (): ReactElement => { export const Route = createFileRoute('/_authenticated')({
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); beforeLoad: () => {
const session = SessionToken.get()
if (!session?.token?.access_token) {
throw redirect({ to: '/auth/login' })
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
return ( return (
<div className="bg-primary-50 min-h-screen flex justify-center"> <div className="bg-primary-50 min-h-screen flex justify-center">
@@ -13,14 +24,12 @@ export const AppLayout: FC = (): ReactElement => {
onClose={() => setMobileSidebarOpen(false)} onClose={() => setMobileSidebarOpen(false)}
/> />
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<header <header
className={ className={
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' + 'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' +
(mobileSidebarOpen ? 'z-0' : 'z-30') (mobileSidebarOpen ? 'z-0' : 'z-30')
} }
> >
<button <button
type="button" type="button"
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer" className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer"
@@ -50,7 +59,5 @@ export const AppLayout: FC = (): ReactElement => {
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default AppLayout;
Loaded 50 of 291 files, more files were not shown because too many files have changed in this diff. Show more