Compare commits

...

8 Commits

Author SHA1 Message Date
1a2a213d0a Ganti Image Logo 2026-02-27 14:57:01 +08:00
1ec10fe623 Fix seed-2 2026-02-26 16:30:22 +08:00
226b0880e6 Fix seed 2026-02-26 16:22:08 +08:00
5d9be8c479 Fix sign in github 2026-02-26 15:07:15 +08:00
e83bea2bc2 Fix sign in, sign out, dan register localhost:3000 2026-02-26 14:48:55 +08:00
95c08681a7 fix localhost 2026-02-25 15:51:48 +08:00
9b015ec84d fix localhost 2026-02-25 15:44:26 +08:00
38b22dd2dd feat: update dashboard components (dashboard-content, help-page, kinerja-divisi)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 10:32:24 +08:00
14 changed files with 152 additions and 122 deletions

3
.gitignore vendored
View File

@@ -33,6 +33,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# Dashboard-MD
Dashboard-MD
# Playwright artifacts
test-results/
playwright-report/

View File

@@ -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"
},

View File

@@ -133,7 +133,18 @@ async function main() {
console.log("Database seeding completed.");
}
main().catch((error) => {
console.error("Error during seeding:", error);
process.exit(1);
});
// Only auto-execute when run directly (not when imported)
const isMainModule =
typeof require !== "undefined"
? require.main === module
: import.meta.path.endsWith("seed.ts");
if (isMainModule) {
main().catch((error) => {
console.error("Error during seeding:", error);
process.exit(1);
});
}
// Export for programmatic use
export { seedAdminUser, seedDemoUsers, main as runSeed };

BIN
public/logo-desa-plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -66,6 +66,12 @@ const eventData = [
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
];
const apbdesData = [
{ name: "Belanja", value: 70, color: "blue" },
{ name: "Pendapatan", value: 90, color: "green" },
{ name: "Pembangunan", value: 50, color: "orange" },
];
export function DashboardContent() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
@@ -480,42 +486,23 @@ export function DashboardContent() {
Grafik APBDes
</Title>
<Stack gap="xs">
<Group align="center" gap="md">
<Text size="sm" fw={500} w={60}>
Belanja
</Text>
<Progress
value={70}
size="lg"
radius="xl"
color="blue"
style={{ flex: 1 }}
/>
</Group>
<Group align="center" gap="md">
<Text size="sm" fw={500} w={60}>
Pendapatan
</Text>
<Progress
value={90}
size="lg"
radius="xl"
color="green"
style={{ flex: 1 }}
/>
</Group>
<Group align="center" gap="md">
<Text size="sm" fw={500} w={60}>
Pembangunan
</Text>
<Progress
value={50}
size="lg"
radius="xl"
color="orange"
style={{ flex: 1 }}
/>
</Group>
{apbdesData.map((data, index) => (
<Grid key={index} align="center">
<Grid.Col span={3}>
<Text size="sm" fw={500}>
{data.name}
</Text>
</Grid.Col>
<Grid.Col span={9}>
<Progress
value={data.value}
size="lg"
radius="xl"
color={data.color}
/>
</Grid.Col>
</Grid>
))}
</Stack>
</Card>
</Stack>

View File

@@ -143,13 +143,6 @@ const HelpPage = () => {
return (
<Container size="lg" py="xl">
<Title order={1} mb="xl" ta="center">
Pusat Bantuan
</Title>
<Text size="lg" color="dimmed" ta="center" mb="xl">
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
</Text>
{/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => (

View File

@@ -122,10 +122,10 @@ const KinerjaDivisi = () => {
// Activity progress statistics
const activityProgressStats = [
{ name: "Selesai", value: 12 },
{ name: "Dikerjakan", value: 8 },
{ name: "Segera Dikerjakan", value: 5 },
{ name: "Dibatalkan", value: 2 },
{ name: "Selesai", value: 12, fill: "#10B981" },
{ name: "Dikerjakan", value: 8, fill: "#F59E0B" },
{ name: "Segera Dikerjakan", value: 5, fill: "#EF4444" },
{ name: "Dibatalkan", value: 2, fill: "#6B7280" },
];
const COLORS = ["#10B981", "#F59E0B", "#EF4444", "#6B7280"];
@@ -204,9 +204,9 @@ const KinerjaDivisi = () => {
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>
@@ -402,9 +402,9 @@ const KinerjaDivisi = () => {
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>
@@ -434,33 +434,27 @@ const KinerjaDivisi = () => {
Progres Kegiatan
</Title>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<PieChart
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
>
<Pie
data={activityProgressStats}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
labelLine
outerRadius={65}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
}
>
{activityProgressStats.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Pie>
/>
<Tooltip
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>

View File

@@ -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 (
<Box className={className}>
{/* Logo */}
<Box
p="md"
style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
>
<Group gap="xs">
<Badge
color="dark"
variant="filled"
size="xl"
radius="md"
py="xs"
px="md"
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
>
DESA
</Badge>
<Badge color="green" variant="filled" size="md" radius="md">
+
</Badge>
</Group>
<Text size="xs" c="dimmed" mt="xs">
Digitalisasi Desa Transparansi Kerja
</Text>
</Box>
<Image src={"/logo-desa-plus.png"} width={201} height={84} />
{/* Search */}
<Box p="md">

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -6,8 +6,22 @@ import { Elysia } from "elysia";
import api from "./api";
import { openInEditor } from "./utils/open-in-editor";
const PORT = process.env.PORT || 3000;
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
}
}
const app = new Elysia().use(api);
if (!isProduction) {
@@ -81,10 +95,27 @@ if (!isProduction) {
getHeader(name: string) {
return this.headers[name.toLowerCase()];
},
writeHead(code: number, headers: Record<string, string>) {
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<string, string>,
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") {
@@ -144,6 +175,11 @@ if (!isProduction) {
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
@@ -159,8 +195,18 @@ if (!isProduction) {
) {
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
else if (pathname.includes("assetlinks.json")) {
if (pathname.includes("assetlinks.json")) {
const srcFilename = pathname.includes("assetlinks.json")
? ".well-known/assetlinks.json"
: filename;
@@ -198,10 +244,11 @@ if (!isProduction) {
});
}
app.listen(3000);
app.listen(PORT);
console.log(
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`,
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
);
export type ApiApp = typeof app;

View File

@@ -2,11 +2,7 @@ import createClient from "openapi-fetch";
import type { paths } from "../../generated/api";
import { VITE_PUBLIC_URL } from "./env";
const baseUrl =
VITE_PUBLIC_URL ||
(typeof window !== "undefined"
? window.location.origin
: "http://localhost:3000");
const baseUrl = VITE_PUBLIC_URL;
export const apiClient = createClient<paths>({
baseUrl: baseUrl,

View File

@@ -1,16 +1,11 @@
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "../../generated/prisma";
import logger from "./logger";
import { VITE_PUBLIC_URL } from "./env";
const baseUrl = process.env.VITE_PUBLIC_URL;
const baseUrl = VITE_PUBLIC_URL;
const prisma = new PrismaClient();
if (!baseUrl) {
logger.error("VITE_PUBLIC_URL is not defined");
throw new Error("VITE_PUBLIC_URL is not defined");
}
// logger.info('Initializing Better Auth with Prisma adapter');
export const auth = betterAuth({
baseURL: baseUrl,
@@ -37,8 +32,24 @@ export const auth = betterAuth({
},
},
},
databaseHooks: {
user: {
create: {
before: async (user) => {
if (user.email === process.env.ADMIN_EMAIL) {
return {
data: {
...user,
role: "admin",
},
};
}
return { data: user };
},
},
},
},
secret: process.env.BETTER_AUTH_SECRET,
trustedOrigins: ["http://localhost:5173", "http://localhost:3000"],
session: {
cookieCache: {
enabled: true,
@@ -48,5 +59,6 @@ export const auth = betterAuth({
},
advanced: {
cookiePrefix: "bun-react",
trustProxy: true,
},
});

View File

@@ -20,10 +20,21 @@ export const getEnv = (key: string, defaultValue = ""): string => {
return defaultValue;
};
export const VITE_PUBLIC_URL = getEnv(
"VITE_PUBLIC_URL",
"http://localhost:3000",
);
export const VITE_PUBLIC_URL = (() => {
// Priority:
// 1. BETTER_AUTH_URL (standard for better-auth)
// 2. VITE_PUBLIC_URL (our app standard)
// 3. window.location.origin (browser fallback)
const envUrl = getEnv("BETTER_AUTH_URL") || getEnv("VITE_PUBLIC_URL");
if (envUrl) return envUrl;
// Fallback for browser
if (typeof window !== "undefined") {
return window.location.origin;
}
return "http://localhost:3000";
})();
export const IS_DEV = (() => {
try {

View File

@@ -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"),