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