From 8d48aa8765a2608cd3f8375c9da838255576cbd7 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Tue, 25 Nov 2025 10:51:44 +0800 Subject: [PATCH] tambahan --- bin/route.generate.ts | 378 +++++++++++++------ bun.lock | 8 +- package.json | 20 +- src/App.tsx | 2 +- src/AppRoutes.tsx | 140 ++++++- src/Landing.tsx | 414 +++++++++++++++++++++ src/clientRoutes.ts | 3 +- src/index.tsx | 35 +- src/pages/Login.tsx | 21 +- src/pages/NotFound.tsx | 12 +- src/pages/dashboard/apikey/apikey_page.tsx | 7 +- src/pages/dashboard/dashboard_layout.tsx | 1 - src/routeTypes.ts | 11 + src/server/routes/auth_route.ts | 1 - src/server/routes/darmasaba.ts | 8 - 15 files changed, 897 insertions(+), 164 deletions(-) create mode 100644 src/Landing.tsx create mode 100644 src/routeTypes.ts delete mode 100644 src/server/routes/darmasaba.ts diff --git a/bin/route.generate.ts b/bin/route.generate.ts index 1391c98..b30b9e7 100644 --- a/bin/route.generate.ts +++ b/bin/route.generate.ts @@ -12,54 +12,115 @@ import { basename, extname, join, relative } from "path"; const PAGES_DIR = join(process.cwd(), "src/pages"); const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx"); +/****************************** + * Prefetch Helper Template + ******************************/ +const PREFETCH_HELPER = ` /** - * ✅ Ubah nama file menjadi PascalCase - * - Support: snake_case, kebab-case, camelCase, PascalCase + * Prefetch lazy component: + * - Hover + * - Visible (viewport) + * - Browser idle */ -const toComponentName = (fileName: string): string => { - return fileName - .replace(/\.[^/.]+$/, "") // hilangkan ekstensi file - .replace(/[_-]+/g, " ") // snake_case & kebab-case → spasi - .replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase → spasi - .replace(/\b\w/g, (c) => c.toUpperCase()) // kapital tiap kata - .replace(/\s+/g, ""); // gabung semua → PascalCase -}; +export function attachPrefetch(el: HTMLElement | null, preload: () => void) { + if (!el) return; + let done = false; -/** - * ✅ Normalisasi nama menjadi path route (kebab-case) - */ + const run = () => { + if (done) return; + done = true; + preload(); + }; + + // 1) On hover + el.addEventListener("pointerenter", run, { once: true }); + + // 2) On visible (IntersectionObserver) + const io = new IntersectionObserver((entries) => { + if (entries && entries[0] && entries[0].isIntersecting) { + run(); + io.disconnect(); + } + }); + io.observe(el); + + // 3) On idle + if ("requestIdleCallback" in window) { + requestIdleCallback(() => run()); + } else { + setTimeout(run, 200); + } +} +`; + +/****************************** + * Component Name Generator + ******************************/ +const toComponentName = (fileName: string): string => + fileName + .replace(/\.[^/.]+$/, "") + .replace(/[_-]+/g, " ") + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/\b\w/g, (c) => c.toUpperCase()) + .replace(/\s+/g, ""); + +/****************************** + * Route Path Normalizer + ******************************/ function toRoutePath(name: string): string { - name = name.replace(/\.[^/.]+$/, ""); // hapus ekstensi + name = name.replace(/\.[^/.]+$/, ""); if (name.toLowerCase() === "home") return "/"; if (name.toLowerCase() === "login") return "/login"; if (name.toLowerCase() === "notfound") return "/*"; - // Hapus prefix/suffix umum - name = name.replace(/_page$/i, "").replace(/^form_/i, ""); + if (name.startsWith("[") && name.endsWith("]")) + return `:${name.slice(1, -1)}`; - // ✅ Normalisasi ke kebab-case + name = name.replace(/_page$/i, "").replace(/^form_/i, ""); return _.kebabCase(name); } -// 🧭 Scan folder pages secara rekursif +/****************************** + * Scan Folder + Validation + Dynamic Duplicate Check + ******************************/ function scan(dir: string): any[] { const items = readdirSync(dir); const routes: any[] = []; + const dynamicParams = new Set(); for (const item of items) { const full = join(dir, item); const stat = statSync(full); if (stat.isDirectory()) { + if (!/^[a-zA-Z0-9_-]+$/.test(item)) { + console.warn(`⚠️ Invalid folder name: ${item}`); + } + routes.push({ name: item, path: _.kebabCase(item), children: scan(full), }); } else if (extname(item) === ".tsx") { + const base = basename(item, ".tsx"); + + if (!/^[a-zA-Z0-9_[\]-]+$/.test(base)) { + console.warn(`⚠️ Invalid file name: ${item}`); + } + + if (base.startsWith("[") && base.endsWith("]")) { + const p = base.slice(1, -1); + if (dynamicParams.has(p)) { + console.error(`❌ Duplicate dynamic param "${p}" in ${dir}`); + process.exit(1); + } + dynamicParams.add(p); + } + routes.push({ - name: basename(item, ".tsx"), + name: base, filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"), }); } @@ -67,163 +128,233 @@ function scan(dir: string): any[] { return routes; } -// 🏗️ Generate JSX dari struktur folder +/****************************** + * Index Detection + ******************************/ +function findIndexFile(folderName: string, children: any[]) { + const lower = folderName.toLowerCase(); + + return ( + children.find((r: any) => r.name.toLowerCase().endsWith("_home")) || + children.find((r: any) => r.name.toLowerCase() === "index") || + children.find((r: any) => r.name.toLowerCase() === `${lower}_page`) + ); +} + +/****************************** + * Generate JSX (Lazy + Prefetch) + ******************************/ function generateJSX(routes: any[], parentPath = ""): string { let jsx = ""; + for (const route of routes) { if (route.children) { - const layout = route.children.find((r: any) => r.name.endsWith("_layout")); + const layout = route.children.find((r: any) => + r.name.endsWith("_layout") + ); if (layout) { - const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout")); - const nested = route.children.filter((r: any) => r !== layout); + const LayoutComp = toComponentName( + layout.name.replace("_layout", "Layout") + ); + + const nested = route.children.filter((x: any) => x !== layout); const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`); - const homeFile = route.children.find((r: any) => - r.name.toLowerCase().endsWith("_home") - ); - const indexRoute = homeFile - ? `} />\n` - : ""; + const indexFile = findIndexFile(route.name, route.children); + + const indexRoute = indexFile + ? `} />` + : `}/>`; jsx += ` - }> - ${indexRoute} - ${nestedRoutes} - + }> + ${indexRoute} + ${nestedRoutes} + `; } else { jsx += generateJSX(route.children, `${parentPath}/${route.path}`); } } else { - const Component = toComponentName(route.name); + const Comp = toComponentName(route.name); const routePath = toRoutePath(route.name); const fullPath = routePath.startsWith("/") ? routePath : `${parentPath}/${routePath}`.replace(/\/+/g, "/"); - jsx += `} />\n`; + jsx += ` + }> + <${Comp}.Component /> + + } + /> + `; } } + return jsx; } -// 🧾 Generate import otomatis +/****************************** + * Lazy Import + Prefetch Injection + ******************************/ function generateImports(routes: any[]): string { - const imports = new Set(); + const list: string[] = []; - function collect(rs: any[]) { + function walk(rs: any[]) { for (const r of rs) { - if (r.children) collect(r.children); + if (r.children) walk(r.children); else { - const Comp = toComponentName(r.name); - imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`); + const C = toComponentName(r.name); + const file = r.filePath.replace(/\.tsx$/, ""); + + list.push(` + const ${C} = { + Component: React.lazy(() => import("./${file}")), + preload: () => import("./${file}") + }; + `); } } } - collect(routes); - return Array.from(imports).join("\n"); + walk(routes); + return list.join("\n"); } +/****************************** + * Generate AppRoutes.tsx + ******************************/ function generateRoutes() { const allRoutes = scan(PAGES_DIR); const imports = generateImports(allRoutes); - const jsxRoutes = generateJSX(allRoutes); + const jsx = generateJSX(allRoutes); - const finalCode = ` -// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY -import { BrowserRouter, Routes, Route } from "react-router-dom"; + let loadingSkeleton = ` + const SkeletonLoading = () => { + return ( +
+ {Array.from({ length: 5 }, (_, i) => ( + + ))} +
+ ); + }; + + ` + + const final = ` +// ⚡ AUTO-GENERATED — DO NOT EDIT +import React from "react"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { Skeleton } from "@mantine/core"; + +${loadingSkeleton} +${PREFETCH_HELPER} ${imports} export default function AppRoutes() { - return ( - - - ${jsxRoutes} - - - ); + return ( + + + ${jsx} + + + ); } `; - writeFileSync(OUTPUT_FILE, finalCode); + writeFileSync(OUTPUT_FILE, final); console.log(`✅ Routes generated → ${OUTPUT_FILE}`); + Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"]); } -// --- Extract untuk clientRoutes.ts --- -const SRC_DIR = path.resolve(process.cwd(), "src"); -const APP_ROUTES_FILE = path.join(SRC_DIR, "AppRoutes.tsx"); +/****************************** + * Extract flat client routes + ******************************/ +const SRC_DIR = path.resolve("src"); +const APP_ROUTES_FILE = join(SRC_DIR, "AppRoutes.tsx"); interface RouteNode { path: string; children: RouteNode[]; } -function getAttributePath(attrs: (t.JSXAttribute | t.JSXSpreadAttribute)[]) { - const pathAttr = attrs.find( - (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "path" }) - ) as t.JSXAttribute | undefined; +function getAttributePath(attrs: any[]) { + const attr = attrs.find( + (a) => t.isJSXAttribute(a) && a.name.name === "path" + ) as any; - if (pathAttr && t.isStringLiteral(pathAttr.value)) return pathAttr.value.value; - return ""; + return attr?.value?.value ?? ""; } function extractRouteNodes(node: t.JSXElement): RouteNode | null { - const opening = node.openingElement; - if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") return null; + const op = node.openingElement; + if (!t.isJSXIdentifier(op.name) || op.name.name !== "Route") return null; - const currentPath = getAttributePath(opening.attributes); + const cur = getAttributePath(op.attributes); const children: RouteNode[] = []; - for (const child of node.children) { - if (t.isJSXElement(child)) { - const childNode = extractRouteNodes(child); - if (childNode) children.push(childNode); + for (const c of node.children) { + if (t.isJSXElement(c)) { + const n = extractRouteNodes(c); + if (n) children.push(n); } } - return { path: currentPath, children }; + return { path: cur, children }; } -function flattenRoutes(node: RouteNode, parentPath = ""): Record { - const record: Record = {}; - let fullPath = node.path; +function flattenRoutes(node: RouteNode, parent = ""): Record { + const r: Record = {}; + let full = node.path; - if (fullPath) { - if (!fullPath.startsWith("/")) { - if (parentPath) { - if (fullPath === "/") fullPath = parentPath; - else fullPath = `${parentPath.replace(/\/$/, "")}/${fullPath}`; - } - if (!fullPath.startsWith("/")) fullPath = `/${fullPath}`; - } - fullPath = fullPath.replace(/\/+/g, "/"); - record[fullPath] = fullPath; + if (full) { + if (!full.startsWith("/")) + full = + parent && full !== "/" + ? `${parent.replace(/\/$/, "")}/${full}` + : "/" + full; + full = full.replace(/\/+/g, "/"); + r[full] = full; } - for (const child of node.children) { - Object.assign(record, flattenRoutes(child, fullPath || parentPath)); - } - return record; + for (const c of node.children) + Object.assign(r, flattenRoutes(c, full || parent)); + + return r; } -function extractRoutes(code: string): Record { +function extractRoutes(code: string) { const ast = parser.parse(code, { sourceType: "module", - plugins: ["typescript", "jsx"], + plugins: ["jsx", "typescript"], }); const routes: Record = {}; + traverse(ast, { - JSXElement(path) { - const opening = path.node.openingElement; - if (t.isJSXIdentifier(opening.name) && opening.name.name === "Routes") { - for (const child of path.node.children) { - if (t.isJSXElement(child)) { - const node = extractRouteNodes(child); - if (node) Object.assign(routes, flattenRoutes(node)); + JSXElement(p) { + const op = p.node.openingElement; + + if (t.isJSXIdentifier(op.name) && op.name.name === "Routes") { + for (const c of p.node.children) { + if (t.isJSXElement(c)) { + const root = extractRouteNodes(c); + if (root) Object.assign(routes, flattenRoutes(root)); } } } @@ -233,28 +364,53 @@ function extractRoutes(code: string): Record { return routes; } -export default function route() { - generateRoutes(); +/****************************** + * Type-Safe Route Builder + ******************************/ +function generateTypeSafe(routes: Record) { + const keys = Object.keys(routes).filter((x) => !x.includes("*")); + const union = keys.map((x) => `"${x}"`).join(" | "); - if (!fs.existsSync(APP_ROUTES_FILE)) { - console.error("❌ AppRoutes.tsx not found in src/"); - process.exit(1); + const code = ` +export type AppRoute = ${union}; + +export function route(path: AppRoute, params?: Record) { + if (!params) return path; + let final = path; + for (const k of Object.keys(params)) { + final = final.replace(":" + k, params[k] + "") as AppRoute; } + return final; +} +`; + + fs.writeFileSync(join(SRC_DIR, "routeTypes.ts"), code); + console.log("📄 routeTypes.ts generated."); +} + +/****************************** + * MAIN + ******************************/ +export default function run() { + generateRoutes(); const code = fs.readFileSync(APP_ROUTES_FILE, "utf-8"); const routes = extractRoutes(code); - console.log("✅ Generated Routes:"); - console.log(routes); + const out = join(SRC_DIR, "clientRoutes.ts"); - const outPath = path.join(SRC_DIR, "clientRoutes.ts"); fs.writeFileSync( - outPath, - `// AUTO-GENERATED FILE\nconst clientRoutes = ${JSON.stringify(routes, null, 2)} as const;\n\nexport default clientRoutes;` + out, + `// AUTO-GENERATED\nconst clientRoutes = ${JSON.stringify( + routes, + null, + 2 + )} as const;\nexport default clientRoutes;` ); - console.log(`📄 clientRoutes.ts saved → ${outPath}`); + console.log(`📄 clientRoutes.ts saved → ${out}`); + + generateTypeSafe(routes); } -route() - +run(); diff --git a/bun.lock b/bun.lock index 7353428..7ea8ef1 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,6 @@ "@mantine/notifications": "^8.3.8", "@prisma/client": "^6.19.0", "@tabler/icons-react": "^3.35.0", - "add": "^2.0.6", "dotenv": "^17.2.3", "elysia": "^1.4.16", "jwt-decode": "^4.0.0", @@ -23,6 +22,7 @@ "react-dom": "^19.2.0", "react-router-dom": "^7.9.6", "swr": "^2.3.6", + "zod": "^4.1.13", }, "devDependencies": { "@babel/parser": "^7.28.5", @@ -148,8 +148,6 @@ "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, ""], - "add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="], - "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -344,7 +342,7 @@ "zhead": ["zhead@2.2.4", "", {}, ""], - "zod": ["zod@3.25.76", "", {}, ""], + "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, ""], @@ -361,5 +359,7 @@ "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, ""], "@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": "bin/nanoid.js" }, ""], + + "@scalar/themes/@scalar/types/zod": ["zod@3.25.76", "", {}, ""], } } diff --git a/package.json b/package.json index e05dd78..f39afe8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@mantine/notifications": "^8.3.8", "@prisma/client": "^6.19.0", "@tabler/icons-react": "^3.35.0", - "add": "^2.0.6", "dotenv": "^17.2.3", "elysia": "^1.4.16", "jwt-decode": "^4.0.0", @@ -30,21 +29,22 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.9.6", - "swr": "^2.3.6" + "swr": "^2.3.6", + "zod": "^4.1.13" }, "devDependencies": { + "prisma": "^6.19.0", + "@types/bun": "latest", + "@types/react": "^19.2.6", + "@types/react-dom": "^19.2.3", + "@types/lodash": "^4.17.21", + "@types/jwt-decode": "^3.1.0", "@babel/parser": "^7.28.5", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@types/babel__traverse": "^7.28.0", - "@types/bun": "latest", - "@types/jwt-decode": "^3.1.0", - "@types/lodash": "^4.17.21", - "@types/react": "^19.2.6", - "@types/react-dom": "^19.2.3", "postcss": "^8.5.6", "postcss-preset-mantine": "^1.18.0", - "postcss-simple-vars": "^7.0.1", - "prisma": "^6.19.0" + "postcss-simple-vars": "^7.0.1" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 9bf5309..f0ca892 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import AppRoutes from "./AppRoutes"; export function App() { return ( - + diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index 10d75bc..19d5e38 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -1,24 +1,136 @@ -// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Login from "./pages/Login"; -import Home from "./pages/Home"; -import ApikeyPage from "./pages/dashboard/apikey/apikey_page"; -import DashboardPage from "./pages/dashboard/dashboard_page"; -import DashboardLayout from "./pages/dashboard/dashboard_layout"; -import NotFound from "./pages/NotFound"; +// ⚡ AUTO-GENERATED — DO NOT EDIT +import React from "react"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { Skeleton } from "@mantine/core"; + +const SkeletonLoading = () => { + return ( +
+ {Array.from({ length: 5 }, (_, i) => ( + + ))} +
+ ); +}; + +/** + * Prefetch lazy component: + * - Hover + * - Visible (viewport) + * - Browser idle + */ +export function attachPrefetch(el: HTMLElement | null, preload: () => void) { + if (!el) return; + let done = false; + + const run = () => { + if (done) return; + done = true; + preload(); + }; + + // 1) On hover + el.addEventListener("pointerenter", run, { once: true }); + + // 2) On visible (IntersectionObserver) + const io = new IntersectionObserver((entries) => { + if (entries && entries[0] && entries[0].isIntersecting) { + run(); + io.disconnect(); + } + }); + io.observe(el); + + // 3) On idle + if ("requestIdleCallback" in window) { + requestIdleCallback(() => run()); + } else { + setTimeout(run, 200); + } +} + +const Login = { + Component: React.lazy(() => import("./pages/Login")), + preload: () => import("./pages/Login"), +}; + +const Home = { + Component: React.lazy(() => import("./pages/Home")), + preload: () => import("./pages/Home"), +}; + +const ApikeyPage = { + Component: React.lazy(() => import("./pages/dashboard/apikey/apikey_page")), + preload: () => import("./pages/dashboard/apikey/apikey_page"), +}; + +const DashboardPage = { + Component: React.lazy(() => import("./pages/dashboard/dashboard_page")), + preload: () => import("./pages/dashboard/dashboard_page"), +}; + +const DashboardLayout = { + Component: React.lazy(() => import("./pages/dashboard/dashboard_layout")), + preload: () => import("./pages/dashboard/dashboard_layout"), +}; + +const NotFound = { + Component: React.lazy(() => import("./pages/NotFound")), + preload: () => import("./pages/NotFound"), +}; export default function AppRoutes() { return ( - } /> - } /> + }> + + + } + /> - }> - } /> - } /> + }> + + + } + /> + + }> + } /> + + }> + + + } + /> + + }> + + + } + /> - } /> + + }> + + + } + /> ); diff --git a/src/Landing.tsx b/src/Landing.tsx new file mode 100644 index 0000000..39014d3 --- /dev/null +++ b/src/Landing.tsx @@ -0,0 +1,414 @@ +import clientRoutes from "./clientRoutes"; + +export function LandingPage() { + return ( + + + + + NexaFlow - Modern AI Solutions + + + + + +
+
+

Transform Your Workflow with AI

+

Powerful automation and intelligent insights to boost your productivity and streamline operations

+ +
+
+ +
+
+

Why Choose NexaFlow?

+
+
+
+

Lightning Fast

+

Experience blazing fast performance with our optimized infrastructure and cutting-edge technology

+
+
+
🔒
+

Secure & Reliable

+

Enterprise-grade security with 99.9% uptime guarantee to keep your data safe and accessible

+
+
+
🎯
+

Smart Analytics

+

Gain actionable insights with AI-powered analytics and make data-driven decisions effortlessly

+
+
+
+
+ +
+
+
+
+

50K+

+

Active Users

+
+
+

99.9%

+

Uptime

+
+
+

24/7

+

Support

+
+
+

150+

+

Integrations

+
+
+
+
+ +
+
+
NexaFlow
+

Empowering businesses with intelligent automation

+
+ 𝕏 + in + f +
+

© 2025 NexaFlow. All rights reserved.

+
+
+ + + ); +} \ No newline at end of file diff --git a/src/clientRoutes.ts b/src/clientRoutes.ts index b137a53..e678d42 100644 --- a/src/clientRoutes.ts +++ b/src/clientRoutes.ts @@ -1,4 +1,4 @@ -// AUTO-GENERATED FILE +// AUTO-GENERATED const clientRoutes = { "/login": "/login", "/": "/", @@ -7,5 +7,4 @@ const clientRoutes = { "/dashboard/dashboard": "/dashboard/dashboard", "/*": "/*" } as const; - export default clientRoutes; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 33be5b7..6e003d2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,15 @@ import Elysia, { t } from "elysia"; import Swagger from "@elysiajs/swagger"; import html from "./index.html"; -import Dashboard from "./server/routes/darmasaba"; import { apiAuth } from "./server/middlewares/apiAuth"; import Auth from "./server/routes/auth_route"; import ApiKeyRoute from "./server/routes/apikey_route"; import type { User } from "generated/prisma"; +import { LandingPage } from "./Landing"; +import { renderToReadableStream } from "react-dom/server"; +import { cors } from "@elysiajs/cors"; + +const PORT = process.env.PORT || 3000; const Docs = new Elysia().use( Swagger({ @@ -25,18 +29,39 @@ const ApiUser = new Elysia({ const Api = new Elysia({ prefix: "/api", }) + .use(apiAuth) .use(ApiKeyRoute) - .use(Dashboard) .use(ApiUser); const app = new Elysia() + .use( + cors({ + origin: "*", + methods: ["GET", "POST", "OPTIONS"], + allowedHeaders: ["Content-Type"], + }), + ) .use(Api) .use(Docs) .use(Auth) - .get("*", html) - .listen(3000, () => { - console.log("Server running at http://localhost:3000"); + .get("/", async () => { + const stream = await renderToReadableStream(); + return new Response(stream, { + headers: { "Content-Type": "text/html" }, + }); + }) + .get("/assets/:name", (ctx) => { + try { + const file = Bun.file(`public/${encodeURIComponent(ctx.params.name)}`); + return new Response(file); + } catch (error) { + return new Response("File not found", { status: 404 }); + } + }) + .get("/*", html) + .listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); }); export type ServerApp = typeof app; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 6e4e502..4bdbbda 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -9,13 +9,16 @@ import { TextInput, Title, } from "@mantine/core"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import apiFetch from "../lib/apiFetch"; +import clientRoutes from "@/clientRoutes"; +import { Navigate } from "react-router-dom"; export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(null); const handleSubmit = async () => { setLoading(true); @@ -41,6 +44,22 @@ export default function Login() { } }; + useEffect(() => { + async function checkSession() { + try { + // backend otomatis baca cookie JWT dari request + const res = await apiFetch.api.user.find.get(); + setIsAuthenticated(res.status === 200); + } catch { + setIsAuthenticated(false); + } + } + checkSession(); + }, []); + + if (isAuthenticated === null) return null; + if (isAuthenticated) return ; + return ( diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index e3ef82c..64799a4 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,7 +1,13 @@ +import { Container, Text, Anchor } from "@mantine/core"; + export default function NotFound() { return ( -
-

404 Not Found

-
+ + 404 Not Found + The page you are looking for does not exist. + + Go back home + + ); } diff --git a/src/pages/dashboard/apikey/apikey_page.tsx b/src/pages/dashboard/apikey/apikey_page.tsx index 98d1ec8..e38e01a 100644 --- a/src/pages/dashboard/apikey/apikey_page.tsx +++ b/src/pages/dashboard/apikey/apikey_page.tsx @@ -39,7 +39,7 @@ function CreateApiKey() { try { setLoading(true); - if (!name || !description ) { + if (!name || !description) { showNotification({ title: "Error", message: "All fields are required", @@ -48,11 +48,12 @@ function CreateApiKey() { return; } - const res = await apiFetch.api.apikey.create.post({ name, description, - expiredAt: expiredAt ? new Date(expiredAt).toISOString() : new Date().toISOString(), + expiredAt: expiredAt + ? new Date(expiredAt).toISOString() + : new Date().toISOString(), }); if (res.status === 200) { diff --git a/src/pages/dashboard/dashboard_layout.tsx b/src/pages/dashboard/dashboard_layout.tsx index 20e3aab..0227547 100644 --- a/src/pages/dashboard/dashboard_layout.tsx +++ b/src/pages/dashboard/dashboard_layout.tsx @@ -60,7 +60,6 @@ export default function DashboardLayout() { defaultValue: true, }); - return ( ) { + if (!params) return path; + let final = path; + for (const k of Object.keys(params)) { + final = final.replace(":" + k, params[k] + "") as AppRoute; + } + return final; +} diff --git a/src/server/routes/auth_route.ts b/src/server/routes/auth_route.ts index 004c7cd..7cb4ae1 100644 --- a/src/server/routes/auth_route.ts +++ b/src/server/routes/auth_route.ts @@ -4,7 +4,6 @@ import Elysia, { t, type Cookie, type HTTPHeaders, type StatusMap } from 'elysia import { type ElysiaCookie } from 'elysia/cookies' import { prisma } from '@/server/lib/prisma' -import type { User } from 'generated/prisma' const secret = process.env.JWT_SECRET if (!secret) { diff --git a/src/server/routes/darmasaba.ts b/src/server/routes/darmasaba.ts deleted file mode 100644 index 3d483eb..0000000 --- a/src/server/routes/darmasaba.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Elysia from "elysia"; - -const Dashboard = new Elysia({ - prefix: "/dashboard" -}) - .get("/apa", () => "Hello World") - -export default Dashboard