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
-16
View File
@@ -1,16 +0,0 @@
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
import { Outlet } from 'react-router-dom';
import { FC, ReactElement } from 'react';
import { ModalLoginProvider } from '@imphnen-frontend-service/utils';
export const AppLayout: FC = (): ReactElement => {
return (
<ModalLoginProvider>
<main className="bg-primary-50 min-h-screen">
<Navbar />
<Outlet />
</main>
</ModalLoginProvider>
);
};
export default AppLayout;
+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>
);
)
-5
View File
@@ -1,5 +0,0 @@
import { LoaderFunctionArgs } from 'react-router';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
return null;
};
+2
View File
@@ -0,0 +1,2 @@
// This file is auto-generated by TanStack Router
export const routeTree = {} as any
+18
View File
@@ -0,0 +1,18 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { Navbar } from '@imphnen-frontend-service/ui/organisms'
import { ModalLoginProvider } from '@imphnen-frontend-service/utils'
export const Route = createRootRoute({
component: RootLayout,
})
function RootLayout() {
return (
<ModalLoginProvider>
<main className="bg-primary-50 min-h-screen">
<Navbar />
<Outlet />
</main>
</ModalLoginProvider>
)
}
@@ -7,7 +7,6 @@ import {
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { useNavigate } from 'react-router';
import { useVerifyEmail } from './use-verify-email';
export const useLogin = () => {
@@ -16,7 +15,6 @@ export const useLogin = () => {
mode: 'all',
});
const navigate = useNavigate();
const { mutate, isPending } = usePostLogin();
const { setLoading, setSession, clearSession } = useAuthStore();
@@ -48,7 +46,7 @@ export const useLogin = () => {
setSession(loginData);
}
navigate(0);
window.location.reload();
},
onError: (error) => {
const errorMessage = error?.response?.data?.message;
@@ -5,12 +5,10 @@ import {
} from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
export const useVerifyEmail = () => {
const [showVerifyModal, setShowVerifyModal] = useState(false);
const [emailToVerify, setEmailToVerify] = useState('');
const navigate = useNavigate();
const postVerifyEmail = usePostVerifyEmail();
@@ -33,7 +31,7 @@ export const useVerifyEmail = () => {
onSuccess: () => {
toast.success('Verifikasi email sukses');
navigate(0);
window.location.reload();
},
onError: (error) =>
toast.error(error.message || 'Verifikasi email gagal'),
@@ -1,16 +1,21 @@
import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, Fragment, ReactElement, useState } from 'react';
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password';
import ModalFormLogin from './_components/form/modal-form-login';
import ModalFormRegister from './_components/form/modal-form-register';
import { GachaItem } from './_components/item/gacha-item';
import { useModalLogin } from '@imphnen-frontend-service/utils';
import { useGachaItemList, useUserCredits, useExecuteGachaRoll } from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
import type { TGachaRollItemDto } from '@imphnen-frontend-service/service';
import { createFileRoute } from '@tanstack/react-router'
import { ArrowDownOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { FC, Fragment, ReactElement, useState } from 'react'
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password'
import ModalFormLogin from './_components/form/modal-form-login'
import ModalFormRegister from './_components/form/modal-form-register'
import { GachaItem } from './_components/item/gacha-item'
import { useModalLogin } from '@imphnen-frontend-service/utils'
import { useGachaItemList, useUserCredits, useExecuteGachaRoll } from '@imphnen-frontend-service/service'
import { toast } from 'sonner'
import type { TGachaRollItemDto } from '@imphnen-frontend-service/service'
export const Components: FC = (): ReactElement => {
export const Route = createFileRoute('/')({
component: GachaHomePage,
})
function GachaHomePage(): ReactElement {
const { showModalLogin, setShowModalLogin } = useModalLogin();
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
const [showModalRegister, setShowModalRegister] = useState(false);
@@ -251,6 +256,4 @@ export const Components: FC = (): ReactElement => {
/>
</Fragment>
);
};
export default Components;
}