feat: migrate all 5 Vite apps from react-router to TanStack Router
Migrated backoffice, hackathon, dimentorin, gacha, qrcampaign, and infra from custom react-router file-based routing to TanStack Router file-based routing following the tanstack-frontend-best-practice convention. Key changes per app: - New routes/ directory with __root.tsx, _public.tsx, _authenticated.tsx - Auth guards via beforeLoad (replaces old middleware.ts) - createFileRoute pattern for all page components - TanStackRouterVite plugin in vite.config for auto route generation - _components/_hooks folders colocated with routes (ignored by router) - routeTree.gen.ts auto-generated on dev/build Convention: - _public/* routes redirect to dashboard if authenticated - _authenticated/* routes redirect to /auth/login if not authenticated - $param for dynamic segments (was [param] in old convention) - _layout suffix for pathless layout routes Removed: - Old src/app/ directories from all apps - Old src/middleware.ts files - Custom convertPagesToRoute utility (no longer needed) - react-router dependency usage (kept in package.json for shared libs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
86d9e759a6
commit
4240e8eb51
@@ -1,12 +0,0 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
|
||||
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
|
||||
Page Not Found
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
The page you are looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<Link
|
||||
to="/hackathon-dashboard"
|
||||
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Go Back Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError();
|
||||
let errorMessage: string;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
errorMessage = error.statusText;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else {
|
||||
console.error(error);
|
||||
errorMessage = 'Unknown error';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
|
||||
<p className="text-xl text-gray-700 mb-2">
|
||||
Sorry, an unexpected error has occurred.
|
||||
</p>
|
||||
<p className="text-gray-500 italic">{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,28 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { middleware } from './middleware';
|
||||
import { StrictMode } from 'react';
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
||||
import {
|
||||
add404PageToRoutesChildren,
|
||||
addErrorElementToRoutes,
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { QueryProvider } from '@imphnen-frontend-service/utils'
|
||||
import { Toaster } from 'sonner'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import './index.css'
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
const errorFiles = import.meta.glob('./app/**/*error.tsx');
|
||||
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
|
||||
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
||||
addErrorElementToRoutes(errorFiles, routes);
|
||||
add404PageToRoutesChildren(notFoundFiles, routes);
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
...routes,
|
||||
loader: middleware,
|
||||
shouldRevalidate: () => true,
|
||||
},
|
||||
]);
|
||||
const rootElement = document.getElementById('root')
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) throw new Error('Failed to find the root element');
|
||||
if (!rootElement) throw new Error('Failed to find the root element')
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
)
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
PERMISSIONS,
|
||||
SessionToken,
|
||||
SessionUser,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||
|
||||
const mappingPublicRoutes = [
|
||||
'/auth/login',
|
||||
'/auth/forgot',
|
||||
'/auth/new-password',
|
||||
];
|
||||
|
||||
const mappingRoutePermissions = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
permissions: [PERMISSIONS.USERS.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/users/create',
|
||||
permissions: [PERMISSIONS.USERS.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/users/update',
|
||||
permissions: [PERMISSIONS.USERS.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/users/detail',
|
||||
permissions: [PERMISSIONS.USERS.READ_DETAIL],
|
||||
},
|
||||
{
|
||||
path: '/roles',
|
||||
permissions: [PERMISSIONS.ROLES.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/roles/create',
|
||||
permissions: [PERMISSIONS.ROLES.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/roles/update',
|
||||
permissions: [PERMISSIONS.ROLES.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/roles/detail',
|
||||
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
|
||||
},
|
||||
{
|
||||
path: '/permissions',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/permissions/create',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/permissions/update',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/permissions/detail',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
|
||||
},
|
||||
];
|
||||
|
||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
const session = SessionUser.get();
|
||||
const session_token = SessionToken.get();
|
||||
const token = session_token?.token?.access_token;
|
||||
const userPermissions =
|
||||
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||
|
||||
if (mappingPublicRoutes.includes(pathname)) {
|
||||
if (token) return redirect('/hackathon-dashboard');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!session) return redirect('/auth/login');
|
||||
|
||||
const matchedRoute = mappingRoutePermissions.find(
|
||||
(route) => route.path === pathname
|
||||
);
|
||||
|
||||
if (matchedRoute) {
|
||||
const hasPermission =
|
||||
!matchedRoute.permissions ||
|
||||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
||||
|
||||
if (!hasPermission) {
|
||||
return '/hackathon-dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
// This file is auto-generated by TanStack Router
|
||||
export const routeTree = {} as any
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <Outlet />,
|
||||
})
|
||||
+18
-11
@@ -1,9 +1,20 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
import { SessionToken } from '@imphnen-frontend-service/service'
|
||||
import { useState } from 'react'
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
export const Route = createFileRoute('/_authenticated')({
|
||||
beforeLoad: () => {
|
||||
const session = SessionToken.get()
|
||||
if (!session?.token?.access_token) {
|
||||
throw redirect({ to: '/auth/login' })
|
||||
}
|
||||
},
|
||||
component: AuthenticatedLayout,
|
||||
})
|
||||
|
||||
function AuthenticatedLayout() {
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||
@@ -13,14 +24,12 @@ export const AppLayout: FC = (): ReactElement => {
|
||||
onClose={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
||||
<header
|
||||
className={
|
||||
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' +
|
||||
(mobileSidebarOpen ? 'z-0' : 'z-30')
|
||||
}
|
||||
>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer"
|
||||
@@ -50,7 +59,5 @@ export const AppLayout: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user