diff --git a/package.json b/package.json index fbe9b89..b835530 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts", + "dev": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts", "lint": "biome check .", "check": "biome check --write .", "format": "biome format --write .", @@ -12,7 +12,7 @@ "test": "bun test __tests__/api", "test:ui": "bun test --ui __tests__/api", "test:e2e": "bun run build && playwright test", - "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'", + "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true", "start": "NODE_ENV=production bun src/index.ts", "seed": "bun prisma/seed.ts" }, diff --git a/public/logo-desa-plus.png b/public/logo-desa-plus.png new file mode 100644 index 0000000..78f4e84 Binary files /dev/null and b/public/logo-desa-plus.png differ diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index f734db2..c25a897 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -1,13 +1,11 @@ import { - Badge, Box, Collapse, - Group, + Image, Input, NavLink as MantineNavLink, Stack, - Text, - useMantineColorScheme, + useMantineColorScheme } from "@mantine/core"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { ChevronDown, ChevronUp, Search } from "lucide-react"; @@ -66,30 +64,7 @@ export function Sidebar({ className }: SidebarProps) { return ( {/* Logo */} - - - - DESA - - - + - - - - Digitalisasi Desa Transparansi Kerja - - + {/* Search */} diff --git a/src/index.ts b/src/index.ts index a8ab986..746d140 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,211 +12,240 @@ const isProduction = process.env.NODE_ENV === "production"; // Auto-seed database in production (ensure admin user exists) if (isProduction && process.env.ADMIN_EMAIL) { - try { - console.log("🌱 Running database seed in production..."); - const { runSeed } = await import("../prisma/seed.ts"); - await runSeed(); - } catch (error) { - console.error("⚠️ Production seed failed:", error); - // Don't crash the server if seed fails - } + try { + console.log("🌱 Running database seed in production..."); + const { runSeed } = await import("../prisma/seed.ts"); + await runSeed(); + } catch (error) { + console.error("⚠️ Production seed failed:", error); + // Don't crash the server if seed fails + } } const app = new Elysia().use(api); if (!isProduction) { - // Development: Use Vite middleware - const { createVite } = await import("./vite"); - const vite = await createVite(); + // Development: Use Vite middleware + const { createVite } = await import("./vite"); + const vite = await createVite(); - // Serve PWA/TWA assets in dev (root and nested path support) - const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); + // Serve PWA/TWA assets in dev (root and nested path support) + const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); - app.post("/__open-in-editor", ({ body }) => { - const { relativePath, lineNumber, columnNumber } = body as { - relativePath: string; - lineNumber: number; - columnNumber: number; - }; + app.post("/__open-in-editor", ({ body }) => { + const { relativePath, lineNumber, columnNumber } = body as { + relativePath: string; + lineNumber: number; + columnNumber: number; + }; - openInEditor(relativePath, { - line: lineNumber, - column: columnNumber, - editor: "antigravity", - }); + openInEditor(relativePath, { + line: lineNumber, + column: columnNumber, + editor: "antigravity", + }); - return { ok: true }; - }); + return { ok: true }; + }); - // Vite middleware for other requests - app.all("*", async ({ request }) => { - const url = new URL(request.url); - const pathname = url.pathname; + // Vite middleware for other requests + app.all("*", async ({ request }) => { + const url = new URL(request.url); + const pathname = url.pathname; - // Serve transformed index.html for root or any path that should be handled by the SPA - if ( - pathname === "/" || - (!pathname.includes(".") && - !pathname.startsWith("/@") && - !pathname.startsWith("/inspector") && - !pathname.startsWith("/__open-stack-frame-in-editor") && - !pathname.startsWith("/api")) - ) { - try { - const htmlPath = path.resolve("src/index.html"); - let html = fs.readFileSync(htmlPath, "utf-8"); - html = await vite.transformIndexHtml(pathname, html); + // Serve transformed index.html for root or any path that should be handled by the SPA + if ( + pathname === "/" || + (!pathname.includes(".") && + !pathname.startsWith("/@") && + !pathname.startsWith("/inspector") && + !pathname.startsWith("/__open-stack-frame-in-editor") && + !pathname.startsWith("/api")) + ) { + try { + const htmlPath = path.resolve("src/index.html"); + let html = fs.readFileSync(htmlPath, "utf-8"); + html = await vite.transformIndexHtml(pathname, html); - return new Response(html, { - headers: { "Content-Type": "text/html" }, - }); - } catch (e) { - console.error(e); - } - } + return new Response(html, { + headers: { "Content-Type": "text/html" }, + }); + } catch (e) { + console.error(e); + } + } - return new Promise((resolve) => { - // Use a Proxy to mock Node.js req because Bun's Request is read-only - const req = new Proxy(request, { - get(target, prop) { - if (prop === "url") return pathname + url.search; - if (prop === "method") return request.method; - if (prop === "headers") - return Object.fromEntries(request.headers as any); - return (target as any)[prop]; - }, - }) as any; + return new Promise((resolve) => { + // Use a Proxy to mock Node.js req because Bun's Request is read-only + const req = new Proxy(request, { + get(target, prop) { + if (prop === "url") return pathname + url.search; + if (prop === "method") return request.method; + if (prop === "headers") + return Object.fromEntries(request.headers as any); + return (target as any)[prop]; + }, + }) as any; - const res = { - statusCode: 200, - setHeader(name: string, value: string) { - this.headers[name.toLowerCase()] = value; - }, - getHeader(name: string) { - return this.headers[name.toLowerCase()]; - }, - headers: {} as Record, - end(data: any) { - // Handle potential Buffer or string data from Vite - let body = data; - if (data instanceof Uint8Array) { - body = data; - } else if (typeof data === "string") { - body = data; - } else if (data) { - body = String(data); - } + const res = { + statusCode: 200, + setHeader(name: string, value: string) { + this.headers[name.toLowerCase()] = value; + }, + getHeader(name: string) { + return this.headers[name.toLowerCase()]; + }, + writeHead(code: number, headers: Record) { + this.statusCode = code; + Object.assign(this.headers, headers); + }, + write(chunk: any, callback?: () => void) { + // Collect chunks for streaming responses + if (!this._chunks) this._chunks = []; + this._chunks.push(chunk); + if (callback) callback(); + return true; // Indicate we can accept more data + }, + headers: {} as Record, + end(data: any) { + // Handle potential Buffer or string data from Vite + let body = data; + // If we have collected chunks from write() calls, combine them + if (this._chunks && this._chunks.length > 0) { + body = Buffer.concat(this._chunks); + } + if (data instanceof Uint8Array) { + body = data; + } else if (typeof data === "string") { + body = data; + } else if (data) { + body = String(data); + } - resolve( - new Response(body || "", { - status: this.statusCode, - headers: this.headers, - }), - ); - }, - // Minimal event emitter mock - once() { - return this; - }, - on() { - return this; - }, - emit() { - return this; - }, - removeListener() { - return this; - }, - } as any; + resolve( + new Response(body || "", { + status: this.statusCode, + headers: this.headers, + }), + ); + }, + // Minimal event emitter mock + once() { + return this; + }, + on() { + return this; + }, + emit() { + return this; + }, + removeListener() { + return this; + }, + } as any; - vite.middlewares(req, res, (err: any) => { - if (err) { - console.error("Vite middleware error:", err); - resolve(new Response(err.stack || err.toString(), { status: 500 })); - return; - } - // If Vite doesn't handle it, return 404 - resolve(new Response("Not Found", { status: 404 })); - }); - }); - }); + vite.middlewares(req, res, (err: any) => { + if (err) { + console.error("Vite middleware error:", err); + resolve(new Response(err.stack || err.toString(), { status: 500 })); + return; + } + // If Vite doesn't handle it, return 404 + resolve(new Response("Not Found", { status: 404 })); + }); + }); + }); } else { - // Production: Final catch-all for static files and SPA fallback - app.all("*", async ({ request }) => { - const url = new URL(request.url); - const pathname = url.pathname; + // Production: Final catch-all for static files and SPA fallback + app.all("*", async ({ request }) => { + const url = new URL(request.url); + const pathname = url.pathname; - // 1. Try exact match in dist - let filePath = path.join( - "dist", - pathname === "/" ? "index.html" : pathname, - ); + // 1. Try exact match in dist + let filePath = path.join( + "dist", + pathname === "/" ? "index.html" : pathname, + ); - // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) - if (isProduction) { - const srcPath = path.join("src", pathname); - if (fs.existsSync(srcPath)) { - filePath = srcPath; - } - } + // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) + if (isProduction) { + const srcPath = path.join("src", pathname); + if (fs.existsSync(srcPath)) { + filePath = srcPath; + } + // Check public folder for static assets + const publicPath = path.join("public", pathname); + if (fs.existsSync(publicPath)) { + filePath = publicPath; + } + } - // 2. If not found and looks like an asset (has extension), try root of dist or src - if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { - if (pathname.includes(".") && !pathname.endsWith("/")) { - const filename = path.basename(pathname); + // 2. If not found and looks like an asset (has extension), try root of dist or src + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + if (pathname.includes(".") && !pathname.endsWith("/")) { + const filename = path.basename(pathname); - // Try root of dist - const fallbackDistPath = path.join("dist", filename); - if ( - fs.existsSync(fallbackDistPath) && - fs.statSync(fallbackDistPath).isFile() - ) { - filePath = fallbackDistPath; - } - // Special handling for PWA files in src - else if (pathname.includes("assetlinks.json")) { - const srcFilename = pathname.includes("assetlinks.json") - ? ".well-known/assetlinks.json" - : filename; - const fallbackSrcPath = path.join("src", srcFilename); - if ( - fs.existsSync(fallbackSrcPath) && - fs.statSync(fallbackSrcPath).isFile() - ) { - filePath = fallbackSrcPath; - } - } - } - } + // Try root of dist + const fallbackDistPath = path.join("dist", filename); + if ( + fs.existsSync(fallbackDistPath) && + fs.statSync(fallbackDistPath).isFile() + ) { + filePath = fallbackDistPath; + } + // Try public folder + else { + const fallbackPublicPath = path.join("public", filename); + if ( + fs.existsSync(fallbackPublicPath) && + fs.statSync(fallbackPublicPath).isFile() + ) { + filePath = fallbackPublicPath; + } + } + // Special handling for PWA files in src + if (pathname.includes("assetlinks.json")) { + const srcFilename = pathname.includes("assetlinks.json") + ? ".well-known/assetlinks.json" + : filename; + const fallbackSrcPath = path.join("src", srcFilename); + if ( + fs.existsSync(fallbackSrcPath) && + fs.statSync(fallbackSrcPath).isFile() + ) { + filePath = fallbackSrcPath; + } + } + } + } - if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { - const file = Bun.file(filePath); - return new Response(file, { - headers: { - Vary: "Accept-Encoding", - }, - }); - } + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + const file = Bun.file(filePath); + return new Response(file, { + headers: { + Vary: "Accept-Encoding", + }, + }); + } - // 3. SPA Fallback: Serve index.html - const indexHtml = path.join("dist", "index.html"); - if (fs.existsSync(indexHtml)) { - return new Response(Bun.file(indexHtml), { - headers: { - Vary: "Accept-Encoding", - }, - }); - } + // 3. SPA Fallback: Serve index.html + const indexHtml = path.join("dist", "index.html"); + if (fs.existsSync(indexHtml)) { + return new Response(Bun.file(indexHtml), { + headers: { + Vary: "Accept-Encoding", + }, + }); + } - return new Response("Not Found", { status: 404 }); - }); + return new Response("Not Found", { status: 404 }); + }); } app.listen(PORT); console.log( - `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, + `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, ); export type ApiApp = typeof app; - diff --git a/src/vite.ts b/src/vite.ts index e8ccfab..5af027a 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite"; export async function createVite() { return createViteServer({ root: process.cwd(), + publicDir: "public", resolve: { alias: { "@": path.resolve(process.cwd(), "./src"),