mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 03:02:03 +00:00
feat: add app shell, file-based routing and bootstrap
This commit is contained in:
parent
7967f7e4c5
commit
33a902034c
16 changed files with 316 additions and 0 deletions
5
src/app-root.tsx
Normal file
5
src/app-root.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Routes } from "@/app-routes";
|
||||
|
||||
export function AppRoot() {
|
||||
return <Routes />;
|
||||
}
|
||||
1
src/app-routes.tsx
Normal file
1
src/app-routes.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Routes } from "@/routing";
|
||||
21
src/app/404.tsx
Normal file
21
src/app/404.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<Box
|
||||
className="flex min-h-screen flex-col items-center justify-center gap-4 bg-(--color-bg-page) px-4"
|
||||
role="main"
|
||||
>
|
||||
<Typography variant="h4" component="h1" className="font-display text-foreground">
|
||||
Page not found
|
||||
</Typography>
|
||||
<Typography variant="body1" className="text-muted-foreground">
|
||||
The page you are looking for does not exist or has been moved.
|
||||
</Typography>
|
||||
<Button component={Link} to="/" variant="contained">
|
||||
Go to dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
2
src/app/_app.tsx
Normal file
2
src/app/_app.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Generouted reserved filename — root shell for all routes. */
|
||||
export { default } from "./_components/root-layout";
|
||||
11
src/app/_components/root-layout.tsx
Normal file
11
src/app/_components/root-layout.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Outlet } from "react-router";
|
||||
|
||||
import { AuthProvider } from "@/providers/auth-provider";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Outlet />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
19
src/main.tsx
Normal file
19
src/main.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { AppRoot } from "@/app-root";
|
||||
import { AppProviders } from "@/providers/AppProviders";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error("Root element #root not found");
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<AppProviders>
|
||||
<AppRoot />
|
||||
</AppProviders>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
74
src/router.ts
Normal file
74
src/router.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Generouted, changes to this file will be overridden
|
||||
/* eslint-disable */
|
||||
|
||||
import { components, hooks, utils } from "@generouted/react-router/client";
|
||||
|
||||
export type Path =
|
||||
| `///src/app`
|
||||
| `///src/app/accounts`
|
||||
| `///src/app/accounts/edit/:id`
|
||||
| `///src/app/accounts/new`
|
||||
| `///src/app/assets`
|
||||
| `///src/app/assets/edit/:id`
|
||||
| `///src/app/assets/new`
|
||||
| `///src/app/calendar`
|
||||
| `///src/app/calendar/edit/:id`
|
||||
| `///src/app/calendar/new`
|
||||
| `///src/app/contacts`
|
||||
| `///src/app/contacts/edit/:id`
|
||||
| `///src/app/contacts/new`
|
||||
| `///src/app/dashboard`
|
||||
| `///src/app/employees`
|
||||
| `///src/app/employees/edit/:id`
|
||||
| `///src/app/employees/new`
|
||||
| `///src/app/followups`
|
||||
| `///src/app/followups/edit/:id`
|
||||
| `///src/app/followups/new`
|
||||
| `///src/app/locations`
|
||||
| `///src/app/locations/edit/:id`
|
||||
| `///src/app/locations/new`
|
||||
| `///src/app/login`
|
||||
| `///src/app/pmschedules`
|
||||
| `///src/app/pmschedules/edit/:id`
|
||||
| `///src/app/pmschedules/new`
|
||||
| `///src/app/settings/dropdowns`
|
||||
| `///src/app/settings/task-templates`
|
||||
| `///src/app/uplifts`
|
||||
| `///src/app/v/:token`
|
||||
| `///src/app/v/:token/dashboard`
|
||||
| `///src/app/v/:token/dispatch/:id`
|
||||
| `///src/app/v/:token/pos`
|
||||
| `///src/app/vendor-pos`
|
||||
| `///src/app/vendor-pos/:id`
|
||||
| `///src/app/vendors`
|
||||
| `///src/app/vendors/edit/:id`
|
||||
| `///src/app/vendors/new`
|
||||
| `///src/app/workorders`
|
||||
| `///src/app/workorders/:id`
|
||||
| `///src/app/workorders/edit/:id`
|
||||
| `///src/app/workorders/new`;
|
||||
|
||||
export type Params = {
|
||||
"///src/app/accounts/edit/:id": { id: string };
|
||||
"///src/app/assets/edit/:id": { id: string };
|
||||
"///src/app/calendar/edit/:id": { id: string };
|
||||
"///src/app/contacts/edit/:id": { id: string };
|
||||
"///src/app/employees/edit/:id": { id: string };
|
||||
"///src/app/followups/edit/:id": { id: string };
|
||||
"///src/app/locations/edit/:id": { id: string };
|
||||
"///src/app/pmschedules/edit/:id": { id: string };
|
||||
"///src/app/v/:token": { token: string };
|
||||
"///src/app/v/:token/dashboard": { token: string };
|
||||
"///src/app/v/:token/dispatch/:id": { token: string; id: string };
|
||||
"///src/app/v/:token/pos": { token: string };
|
||||
"///src/app/vendor-pos/:id": { id: string };
|
||||
"///src/app/vendors/edit/:id": { id: string };
|
||||
"///src/app/workorders/:id": { id: string };
|
||||
"///src/app/workorders/edit/:id": { id: string };
|
||||
};
|
||||
|
||||
export type ModalPath = never;
|
||||
|
||||
export const { Link, Navigate } = components<Path, Params>();
|
||||
export const { useModals, useNavigate, useParams } = hooks<Path, Params, ModalPath>();
|
||||
export const { redirect } = utils<Path, Params>();
|
||||
52
src/routing/app-shell.tsx
Normal file
52
src/routing/app-shell.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { ComponentType, Suspense } from "react";
|
||||
import { Outlet, useLocation } from "react-router";
|
||||
import type { RouteObject } from "react-router";
|
||||
|
||||
import type { RouteModule } from "@/routing/route-module";
|
||||
|
||||
function Modals({ modalRoutes }: { modalRoutes: Record<string, ComponentType> }) {
|
||||
const modalPath = useLocation().state?.modal as string | undefined;
|
||||
const Modal = modalPath ? modalRoutes[modalPath] : undefined;
|
||||
|
||||
if (!Modal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Modal />;
|
||||
}
|
||||
|
||||
export function createAppShellRoute(
|
||||
appLayout: RouteModule | undefined,
|
||||
modalRoutes: Record<string, ComponentType>,
|
||||
): Pick<RouteObject, "Component" | "ErrorBoundary" | "loader"> {
|
||||
const AppLayout = appLayout?.default ?? Outlet;
|
||||
|
||||
function Layout() {
|
||||
return (
|
||||
<>
|
||||
<AppLayout />
|
||||
<Modals modalRoutes={modalRoutes} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppShell() {
|
||||
const Pending = appLayout?.Pending;
|
||||
|
||||
if (Pending) {
|
||||
return (
|
||||
<Suspense fallback={<Pending />}>
|
||||
<Layout />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <Layout />;
|
||||
}
|
||||
|
||||
return {
|
||||
Component: appLayout?.default ? AppShell : Layout,
|
||||
ErrorBoundary: appLayout?.Catch,
|
||||
loader: appLayout?.Loader,
|
||||
};
|
||||
}
|
||||
27
src/routing/build-route-tree.ts
Normal file
27
src/routing/build-route-tree.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Fragment, type ComponentType } from "react";
|
||||
import type { RouteObject } from "react-router";
|
||||
import {
|
||||
generateModalRoutes,
|
||||
generatePreservedRoutes,
|
||||
generateRegularRoutes,
|
||||
} from "@generouted/react-router/core";
|
||||
|
||||
import { MODALS, PRESERVED, ROUTES } from "@/routing/glob-imports";
|
||||
import { remapAppToPages } from "@/routing/remap-routes";
|
||||
import { createLazyRoute, type RouteModule } from "@/routing/route-module";
|
||||
import { createAppShellRoute } from "@/routing/app-shell";
|
||||
|
||||
const preservedRoutes = generatePreservedRoutes(remapAppToPages(PRESERVED));
|
||||
const modalRoutes = generateModalRoutes(remapAppToPages(MODALS)) as Record<string, ComponentType>;
|
||||
|
||||
const regularRoutes = generateRegularRoutes(remapAppToPages(ROUTES), (moduleLoader, key) =>
|
||||
createLazyRoute(moduleLoader as () => Promise<RouteModule>, key),
|
||||
);
|
||||
|
||||
const appLayout = preservedRoutes?.["_app"] as RouteModule | undefined;
|
||||
const notFound = preservedRoutes?.["404"] as RouteModule | undefined;
|
||||
|
||||
const appShell = createAppShellRoute(appLayout, modalRoutes);
|
||||
const fallback: RouteObject = { path: "*", Component: notFound?.default ?? Fragment };
|
||||
|
||||
export const routes: RouteObject[] = [{ ...appShell, children: [...regularRoutes, fallback] }];
|
||||
8
src/routing/constants.ts
Normal file
8
src/routing/constants.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/** Vite glob root for file-based routes (must match import.meta.glob literals in glob-imports.ts). */
|
||||
export const ROUTES_ROOT = "/src/app";
|
||||
|
||||
/**
|
||||
* Generouted core derives URL paths assuming this prefix.
|
||||
* See remap-routes.ts — we scan `/src/app/` but remap keys to `/src/pages/` at runtime.
|
||||
*/
|
||||
export const GENEROUTED_ROOT = "/src/pages";
|
||||
13
src/routing/glob-imports.ts
Normal file
13
src/routing/glob-imports.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { RouteModule } from "@/routing/route-module";
|
||||
|
||||
/** Paths must stay literal — Vite analyzes import.meta.glob at build time. */
|
||||
const PRESERVED = import.meta.glob<RouteModule>("/src/app/{_app,404}.{jsx,tsx}", { eager: true });
|
||||
const MODALS = import.meta.glob<RouteModule>("/src/app/**/[+]*.{jsx,tsx}", { eager: true });
|
||||
const ROUTES = import.meta.glob<RouteModule>([
|
||||
"/src/app/**/[A-Za-z0-9[-]*.{jsx,tsx}",
|
||||
"/src/app/**/_layout.{jsx,tsx}",
|
||||
"!/src/app/404.{jsx,tsx}",
|
||||
"!/src/app/**/_*/**",
|
||||
]);
|
||||
|
||||
export { MODALS, PRESERVED, ROUTES };
|
||||
2
src/routing/index.ts
Normal file
2
src/routing/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Routes } from "@/routing/router";
|
||||
export { routes } from "@/routing/build-route-tree";
|
||||
17
src/routing/remap-routes.ts
Normal file
17
src/routing/remap-routes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { GENEROUTED_ROOT, ROUTES_ROOT } from "@/routing/constants";
|
||||
|
||||
/**
|
||||
* Generouted's `generateRegularRoutes` / `generatePreservedRoutes` strip `/src/pages/`
|
||||
* when building URL segments. Our views live under `/src/app/` (architecture convention),
|
||||
* so we remap glob keys before passing them to the core.
|
||||
*
|
||||
* Ref: https://github.com/oedotme/generouted/tree/main/examples/react-router-custom-path
|
||||
*/
|
||||
export function remapAppToPages<T>(files: Record<string, T>): Record<string, T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).map(([key, value]) => [
|
||||
key.replace(`${ROUTES_ROOT}/`, `${GENEROUTED_ROOT}/`),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
}
|
||||
45
src/routing/route-module.tsx
Normal file
45
src/routing/route-module.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { ComponentType, Fragment, Suspense } from "react";
|
||||
import type { RouteObject } from "react-router";
|
||||
|
||||
export type RouteModule = {
|
||||
default?: ComponentType;
|
||||
Pending?: ComponentType;
|
||||
Catch?: ComponentType;
|
||||
Loader?: RouteObject["loader"];
|
||||
Action?: RouteObject["action"];
|
||||
};
|
||||
|
||||
type ModuleLoader = () => Promise<RouteModule>;
|
||||
|
||||
export function createLazyRoute(
|
||||
moduleLoader: ModuleLoader,
|
||||
key: string,
|
||||
): Pick<RouteObject, "index" | "lazy"> {
|
||||
const index =
|
||||
/index\.(jsx|tsx)$/.test(key) && !key.includes("pages/index") ? { index: true as const } : {};
|
||||
|
||||
return {
|
||||
...index,
|
||||
lazy: async () => {
|
||||
const loaded = await moduleLoader();
|
||||
const PageComponent = loaded?.default ?? Fragment;
|
||||
const Pending = loaded?.Pending;
|
||||
|
||||
const Page = () =>
|
||||
Pending ? (
|
||||
<Suspense fallback={<Pending />}>
|
||||
<PageComponent />
|
||||
</Suspense>
|
||||
) : (
|
||||
<PageComponent />
|
||||
);
|
||||
|
||||
return {
|
||||
Component: Page,
|
||||
ErrorBoundary: loaded?.Catch,
|
||||
loader: loaded?.Loader,
|
||||
action: loaded?.Action,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
9
src/routing/router.tsx
Normal file
9
src/routing/router.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||
|
||||
import { routes } from "@/routing/build-route-tree";
|
||||
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
export function Routes() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
10
src/vite-env.d.ts
vendored
Normal file
10
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_API_TARGET: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue