diff --git a/.versionrc.json b/.versionrc.json index 0c69c73..dfae449 100644 --- a/.versionrc.json +++ b/.versionrc.json @@ -1,16 +1,16 @@ { - "types": [ - { "type": "feat", "section": "🌟 Enhancements" }, - { "type": "fix", "section": "🐛 Bug fixes" }, - { "type": "chore", "hidden": false, "section": "📝 Chore" }, - { "type": "docs", "section": "📚 Documentation" }, - { "type": "style", "hidden": true }, - { "type": "refactor", "section": "🛠️ Code Refactor" }, - { "type": "perf", "hidden": false, "section": "🚀 Code Refactor" }, - { "type": "test", "section": "📝 Testing Code" }, - { "type": "ci", "section": "📈 Project changes" } - ], - "commitUrlFormat": "https://git.tuffraid.net/cowch/Lst_Backend/commits{{hash}}", - "compareUrlFormat": "https://git.tuffraid.net/cowch/Lst_Backend/compare/{{previousTag}}...{{currentTag}}", - "header": "# All notable changes to the LST project will be documented in this file.\n`" + "types": [ + {"type": "feat", "section": "🌟 Enhancements"}, + {"type": "fix", "section": "🐛 Bug fixes"}, + {"type": "chore", "hidden": false, "section": "📝 Chore"}, + {"type": "docs", "section": "📚 Documentation"}, + {"type": "style", "hidden": true}, + {"type": "refactor", "section": "🛠️ Code Refactor"}, + {"type": "perf", "hidden": false, "section": "🚀 Code Refactor"}, + {"type": "test", "section": "📝 Testing Code"}, + {"type": "ci", "section": "📈 Project changes"} + ], + "commitUrlFormat": "https://git.tuffraid.net/cowch/lstV2/commits{{hash}}", + "compareUrlFormat": "https://git.tuffraid.net/cowch/lstV2/compare/{{previousTag}}...{{currentTag}}", + "header": "# All CHanges to LST can be found below.\n`" } diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..b94b87b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,13 @@ - + - - - - - Vite + React + TS - - -
- - + + + + + Logistics Support Tool + + +
+ + diff --git a/frontend/package.json b/frontend/package.json index 41fc443..4da93fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-slot": "^1.1.2", "@tailwindcss/vite": "^4.0.6", "@tanstack/react-query": "^5.66.5", + "@tanstack/react-router": "^1.106.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", @@ -27,6 +28,8 @@ }, "devDependencies": { "@eslint/js": "^9.19.0", + "@tanstack/router-devtools": "^1.106.0", + "@tanstack/router-plugin": "^1.106.0", "@types/node": "^22.13.4", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", diff --git a/frontend/public/lst.ico b/frontend/public/lst.ico new file mode 100644 index 0000000..cfd8d4c Binary files /dev/null and b/frontend/public/lst.ico differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 2f75eac..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import LoginForm from "./components/LoginForm"; -import {Button} from "./components/ui/button"; -import {useSession} from "./lib/hooks/useSession"; -import {useLogout} from "./lib/hooks/useLogout"; -import "./styles.css"; - -function App() { - const {session, status} = useSession(); - const logout = useLogout(); - - if (!session || status === "error") { - return ( -

- no session please login -

- ); - } - - return ( - <> - {/*

Logged in user: {session.user.username}

*/} - <>Session: {JSON.stringify(session)} -

Status: {JSON.stringify(status)}

-

- -

- - ); -} - -export default App; diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx similarity index 95% rename from frontend/src/components/LoginForm.tsx rename to frontend/src/components/auth/LoginForm.tsx index 079168c..51772bd 100644 --- a/frontend/src/components/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -1,5 +1,5 @@ import {useState} from "react"; -import {useSessionStore} from "../lib/store/sessionStore"; +import {useSessionStore} from "../../lib/store/sessionStore"; import {useQueryClient} from "@tanstack/react-query"; const LoginForm = () => { @@ -36,7 +36,7 @@ const LoginForm = () => { setSession(data.data.token); // Refetch the session data to reflect the logged-in state - queryClient.invalidateQueries(["session"]); + queryClient.invalidateQueries(); setUsername(""); setPassword(""); diff --git a/frontend/src/lib/hooks/useSession.ts b/frontend/src/lib/hooks/useSession.ts index 6cd8f8f..d3309b1 100644 --- a/frontend/src/lib/hooks/useSession.ts +++ b/frontend/src/lib/hooks/useSession.ts @@ -15,7 +15,7 @@ const fetchSession = async () => { Authorization: `Bearer ${token}`, }, }); - console.log(res); + // console.log(res); if (!res.ok) { throw new Error("Session not found"); } @@ -31,7 +31,7 @@ export const useSession = () => { queryKey: ["session"], queryFn: fetchSession, enabled: !!token, // Prevents query if token is null - staleTime: 5 * 60 * 1000, // 5 mins + staleTime: 60 * 1000, gcTime: 10 * 60 * 1000, // 10 mins refetchOnWindowFocus: true, }); @@ -47,3 +47,5 @@ export const useSession = () => { return {session: data && token ? {user: data.user, token: data.token} : null, status, error}; }; + +export type SessionType = ReturnType; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2bf9b3f..3430fdb 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,13 +1,38 @@ import {StrictMode} from "react"; -import {createRoot} from "react-dom/client"; +import ReactDOM from "react-dom/client"; import "./styles.css"; -import App from "./App.tsx"; -import {SessionProvider} from "./components/providers/Providers.tsx"; -createRoot(document.getElementById("root")!).render( - - - - - -); +import {SessionProvider} from "./components/providers/Providers.tsx"; +import {RouterProvider, createRouter} from "@tanstack/react-router"; + +// Import the generated route tree +import {routeTree} from "./routeTree.gen"; +import {useSession} from "./lib/hooks/useSession.ts"; + +// Create a new router instance +const router = createRouter({routeTree, context: {sessionType: undefined!}}); + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +function App() { + const sessionType = useSession(); + return ; +} + +// Render the app +const rootElement = document.getElementById("root")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + + + ); +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts new file mode 100644 index 0000000..cfcd7d3 --- /dev/null +++ b/frontend/src/routeTree.gen.ts @@ -0,0 +1,311 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as LoginImport } from './routes/login' +import { Route as AboutImport } from './routes/about' +import { Route as AuthImport } from './routes/_auth' +import { Route as IndexImport } from './routes/index' +import { Route as OcpIndexImport } from './routes/ocp/index' +import { Route as OcpLotsImport } from './routes/ocp/lots' +import { Route as OcpLineIDImport } from './routes/ocp/$lineID' +import { Route as AuthProfileImport } from './routes/_auth/profile' +import { Route as AuthDashboardImport } from './routes/_auth/dashboard' + +// Create/Update Routes + +const LoginRoute = LoginImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRoute, +} as any) + +const AboutRoute = AboutImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRoute, +} as any) + +const AuthRoute = AuthImport.update({ + id: '/_auth', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const OcpIndexRoute = OcpIndexImport.update({ + id: '/ocp/', + path: '/ocp/', + getParentRoute: () => rootRoute, +} as any) + +const OcpLotsRoute = OcpLotsImport.update({ + id: '/ocp/lots', + path: '/ocp/lots', + getParentRoute: () => rootRoute, +} as any) + +const OcpLineIDRoute = OcpLineIDImport.update({ + id: '/ocp/$lineID', + path: '/ocp/$lineID', + getParentRoute: () => rootRoute, +} as any) + +const AuthProfileRoute = AuthProfileImport.update({ + id: '/profile', + path: '/profile', + getParentRoute: () => AuthRoute, +} as any) + +const AuthDashboardRoute = AuthDashboardImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => AuthRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_auth': { + id: '/_auth' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthImport + parentRoute: typeof rootRoute + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutImport + parentRoute: typeof rootRoute + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginImport + parentRoute: typeof rootRoute + } + '/_auth/dashboard': { + id: '/_auth/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof AuthDashboardImport + parentRoute: typeof AuthImport + } + '/_auth/profile': { + id: '/_auth/profile' + path: '/profile' + fullPath: '/profile' + preLoaderRoute: typeof AuthProfileImport + parentRoute: typeof AuthImport + } + '/ocp/$lineID': { + id: '/ocp/$lineID' + path: '/ocp/$lineID' + fullPath: '/ocp/$lineID' + preLoaderRoute: typeof OcpLineIDImport + parentRoute: typeof rootRoute + } + '/ocp/lots': { + id: '/ocp/lots' + path: '/ocp/lots' + fullPath: '/ocp/lots' + preLoaderRoute: typeof OcpLotsImport + parentRoute: typeof rootRoute + } + '/ocp/': { + id: '/ocp/' + path: '/ocp' + fullPath: '/ocp' + preLoaderRoute: typeof OcpIndexImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +interface AuthRouteChildren { + AuthDashboardRoute: typeof AuthDashboardRoute + AuthProfileRoute: typeof AuthProfileRoute +} + +const AuthRouteChildren: AuthRouteChildren = { + AuthDashboardRoute: AuthDashboardRoute, + AuthProfileRoute: AuthProfileRoute, +} + +const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '': typeof AuthRouteWithChildren + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/dashboard': typeof AuthDashboardRoute + '/profile': typeof AuthProfileRoute + '/ocp/$lineID': typeof OcpLineIDRoute + '/ocp/lots': typeof OcpLotsRoute + '/ocp': typeof OcpIndexRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '': typeof AuthRouteWithChildren + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/dashboard': typeof AuthDashboardRoute + '/profile': typeof AuthProfileRoute + '/ocp/$lineID': typeof OcpLineIDRoute + '/ocp/lots': typeof OcpLotsRoute + '/ocp': typeof OcpIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_auth': typeof AuthRouteWithChildren + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/_auth/dashboard': typeof AuthDashboardRoute + '/_auth/profile': typeof AuthProfileRoute + '/ocp/$lineID': typeof OcpLineIDRoute + '/ocp/lots': typeof OcpLotsRoute + '/ocp/': typeof OcpIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/about' + | '/login' + | '/dashboard' + | '/profile' + | '/ocp/$lineID' + | '/ocp/lots' + | '/ocp' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/about' + | '/login' + | '/dashboard' + | '/profile' + | '/ocp/$lineID' + | '/ocp/lots' + | '/ocp' + id: + | '__root__' + | '/' + | '/_auth' + | '/about' + | '/login' + | '/_auth/dashboard' + | '/_auth/profile' + | '/ocp/$lineID' + | '/ocp/lots' + | '/ocp/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AuthRoute: typeof AuthRouteWithChildren + AboutRoute: typeof AboutRoute + LoginRoute: typeof LoginRoute + OcpLineIDRoute: typeof OcpLineIDRoute + OcpLotsRoute: typeof OcpLotsRoute + OcpIndexRoute: typeof OcpIndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AuthRoute: AuthRouteWithChildren, + AboutRoute: AboutRoute, + LoginRoute: LoginRoute, + OcpLineIDRoute: OcpLineIDRoute, + OcpLotsRoute: OcpLotsRoute, + OcpIndexRoute: OcpIndexRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/_auth", + "/about", + "/login", + "/ocp/$lineID", + "/ocp/lots", + "/ocp/" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/_auth": { + "filePath": "_auth.tsx", + "children": [ + "/_auth/dashboard", + "/_auth/profile" + ] + }, + "/about": { + "filePath": "about.tsx" + }, + "/login": { + "filePath": "login.tsx" + }, + "/_auth/dashboard": { + "filePath": "_auth/dashboard.tsx", + "parent": "/_auth" + }, + "/_auth/profile": { + "filePath": "_auth/profile.tsx", + "parent": "/_auth" + }, + "/ocp/$lineID": { + "filePath": "ocp/$lineID.tsx" + }, + "/ocp/lots": { + "filePath": "ocp/lots.tsx" + }, + "/ocp/": { + "filePath": "ocp/index.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx new file mode 100644 index 0000000..5358d87 --- /dev/null +++ b/frontend/src/routes/__root.tsx @@ -0,0 +1,42 @@ +import {createRootRouteWithContext, Link, Outlet} from "@tanstack/react-router"; +import {TanStackRouterDevtools} from "@tanstack/router-devtools"; +import {SessionType} from "../lib/hooks/useSession"; + +type RouterContext = { + sessionType: SessionType; +}; +// same as the layout +export const Route = createRootRouteWithContext()({ + component: () => { + return ( + <> +
+ + Home + {" "} + + About + + + dashboard + + + {({isActive}) => <>Profile {isActive && "~"}} + + + {({isActive}) => <>OCP {isActive && "~"}} + +
+
+ + + + + ); + }, +}); diff --git a/frontend/src/routes/_auth.tsx b/frontend/src/routes/_auth.tsx new file mode 100644 index 0000000..43aa3c1 --- /dev/null +++ b/frontend/src/routes/_auth.tsx @@ -0,0 +1,12 @@ +import {createFileRoute, redirect} from "@tanstack/react-router"; + +// src/routes/_authenticated.tsx +export const Route = createFileRoute("/_auth")({ + beforeLoad: async ({context}) => { + if (!context.sessionType.session) { + throw redirect({ + to: "/", + }); + } + }, +}); diff --git a/frontend/src/routes/_auth/dashboard.tsx b/frontend/src/routes/_auth/dashboard.tsx new file mode 100644 index 0000000..09dd879 --- /dev/null +++ b/frontend/src/routes/_auth/dashboard.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/dashboard')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/_auth/dashboard"!
+} diff --git a/frontend/src/routes/_auth/profile.tsx b/frontend/src/routes/_auth/profile.tsx new file mode 100644 index 0000000..aeb10b9 --- /dev/null +++ b/frontend/src/routes/_auth/profile.tsx @@ -0,0 +1,9 @@ +import {createFileRoute} from "@tanstack/react-router"; + +export const Route = createFileRoute("/_auth/profile")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/profile"!
; +} diff --git a/frontend/src/routes/about.tsx b/frontend/src/routes/about.tsx new file mode 100644 index 0000000..baad8db --- /dev/null +++ b/frontend/src/routes/about.tsx @@ -0,0 +1,13 @@ +import {createFileRoute} from "@tanstack/react-router"; + +export const Route = createFileRoute("/about")({ + component: About, +}); + +function About() { + return ( +
+

About page to come.

+
+ ); +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx new file mode 100644 index 0000000..6074078 --- /dev/null +++ b/frontend/src/routes/index.tsx @@ -0,0 +1,29 @@ +import {createFileRoute} from "@tanstack/react-router"; +import LoginForm from "../components/auth/LoginForm"; +import {Button} from "../components/ui/button"; +import {useLogout} from "../lib/hooks/useLogout"; +import {useSession} from "../lib/hooks/useSession"; + +export const Route = createFileRoute("/")({ + component: Index, +}); + +function Index() { + const {session} = useSession(); + const logout = useLogout(); + return ( +
+

Welcome Home!

+

+

+ {session ? ( + <> + {" "} + + ) : ( + + )} +

+
+ ); +} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx new file mode 100644 index 0000000..71be4d8 --- /dev/null +++ b/frontend/src/routes/login.tsx @@ -0,0 +1,38 @@ +import {createFileRoute, useRouter} from "@tanstack/react-router"; +import {isAuthenticated, signIn, signOut} from "../utils/auth"; +import {Button} from "../components/ui/button"; + +export const Route = createFileRoute("/login")({ + component: RouteComponent, +}); + +function RouteComponent() { + const router = useRouter(); + return ( +
+

Ligin

+ {isAuthenticated() ? ( + <> +

Hello User!

+ + + ) : ( + + )} +
+ ); +} diff --git a/frontend/src/routes/ocp/$lineID.tsx b/frontend/src/routes/ocp/$lineID.tsx new file mode 100644 index 0000000..6dcbd5c --- /dev/null +++ b/frontend/src/routes/ocp/$lineID.tsx @@ -0,0 +1,17 @@ +import {createFileRoute} from "@tanstack/react-router"; + +export const Route = createFileRoute("/ocp/$lineID")({ + component: RouteComponent, + loader: async ({params}) => { + await new Promise((r) => setTimeout(r, 1500)); + //throw new Error(); + return {lineID: params.lineID}; + }, + pendingComponent: () =>
Loading....
, + errorComponent: () =>
Error....
, +}); + +function RouteComponent() { + const {lineID} = Route.useLoaderData(); + return
Hello "/ocp/{lineID}"!
; +} diff --git a/frontend/src/routes/ocp/index.tsx b/frontend/src/routes/ocp/index.tsx new file mode 100644 index 0000000..9af23da --- /dev/null +++ b/frontend/src/routes/ocp/index.tsx @@ -0,0 +1,34 @@ +import {createFileRoute, Link} from "@tanstack/react-router"; + +export const Route = createFileRoute("/ocp/")({ + component: RouteComponent, + validateSearch: (search) => { + return { + q: (search.q as string) || "", + }; + }, + loaderDeps: ({search: {q}}) => ({q}), + loader: async ({deps: {q}}) => { + return {line: q}; + }, +}); + +function RouteComponent() { + const {line} = Route.useLoaderData(); + + const lines = ["l", "2", "3"]; + return ( +
+

Hello "/ocp/{line}/something"!

+ {lines.map((line) => { + return ( +
+ + Post + +
+ ); + })} +
+ ); +} diff --git a/frontend/src/routes/ocp/lots.tsx b/frontend/src/routes/ocp/lots.tsx new file mode 100644 index 0000000..4011f02 --- /dev/null +++ b/frontend/src/routes/ocp/lots.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/ocp/lots')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/ocp/lots"!
+} diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100644 index 0000000..67f6a58 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,11 @@ +export function isAuthenticated() { + return localStorage.getItem("isAuthenticated") === "true"; +} + +export function signIn() { + return localStorage.setItem("isAuthenticated", "true"); +} + +export function signOut() { + return localStorage.removeItem("isAuthenticated"); +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 03c513b..383b219 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,11 +1,12 @@ import {defineConfig} from "vite"; import react from "@vitejs/plugin-react-swc"; import tailwindcss from "@tailwindcss/vite"; +import {TanStackRouterVite} from "@tanstack/router-plugin/vite"; import path from "path"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), TanStackRouterVite({autoCodeSplitting: true})], // build: { // outDir: path.resolve(__dirname, "../../dist/frontend/dist"), // emptyOutDir: true, diff --git a/package.json b/package.json index 55ab607..9e446d0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build:server": "cd apps/server && bun build index.js --outdir ../../dist/server", "build:ocme": "rimraf dist/ocme && cd apps/ocme && bun build index.js --outdir ../../dist/ocme", "build:front": "cd frontend && rimraf frontend/dist && bun run build", - "start": "bun --env-file .env run ./server/index.js", + "start": "cd server && bun run --env-file ../.env ./index.js", "commit": "cz", "clean": "rimraf dist/server", "deploy": "standard-version --conventional-commits" diff --git a/server/src/app.ts b/server/src/app.ts index 51a49c6..9a0b0eb 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -52,8 +52,8 @@ routes.forEach((route) => { // return c.json({success: true, message: "is authenticated"}); // }); -app.get("*", serveStatic({root: "./frontend/dist"})); -app.get("*", serveStatic({path: "./frontend/dist/index.html"})); +app.get("*", serveStatic({root: "../frontend/dist"})); +app.get("*", serveStatic({path: "../frontend/dist/index.html"})); export default app;