Progress Tampilan UI Dashboard Desa Plus NOC
This commit is contained in:
302
PromptDashboard.md
Normal file
302
PromptDashboard.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
Buat halaman dashboard admin modern untuk sistem pemerintahan desa bernama **Darmasaba Dashboard NOC**.
|
||||||
|
|
||||||
|
Gunakan stack berikut:
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
* React 19
|
||||||
|
* Bun runtime
|
||||||
|
* Vite
|
||||||
|
* TailwindCSS
|
||||||
|
* Mantine UI
|
||||||
|
* Mantine Charts atau Recharts
|
||||||
|
* Tabler Icons
|
||||||
|
* TanStack Router
|
||||||
|
* Dayjs
|
||||||
|
|
||||||
|
UI harus modular dengan reusable components.
|
||||||
|
|
||||||
|
Gunakan **TailwindCSS sebagai styling utama** dengan warna dari konfigurasi berikut:
|
||||||
|
|
||||||
|
Primary:
|
||||||
|
darmasaba-navy (#1E3A5F)
|
||||||
|
|
||||||
|
Secondary:
|
||||||
|
darmasaba-blue (#3B82F6)
|
||||||
|
|
||||||
|
Success:
|
||||||
|
#22C55E
|
||||||
|
|
||||||
|
Warning:
|
||||||
|
#FACC15
|
||||||
|
|
||||||
|
Danger:
|
||||||
|
#EF4444
|
||||||
|
|
||||||
|
Background:
|
||||||
|
#F5F8FB
|
||||||
|
|
||||||
|
Dashboard harus memiliki **Light Mode dan Dark Mode**.
|
||||||
|
|
||||||
|
Dark Mode Color Rules:
|
||||||
|
background: #0F172A
|
||||||
|
card: #1E293B
|
||||||
|
border: #334155
|
||||||
|
text: #E2E8F0
|
||||||
|
|
||||||
|
Card style:
|
||||||
|
|
||||||
|
* rounded-xl
|
||||||
|
* soft shadow
|
||||||
|
* padding besar
|
||||||
|
* border subtle
|
||||||
|
* smooth hover animation
|
||||||
|
|
||||||
|
Gunakan grid layout responsive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
SECTION 1 — PROGRAM KEGIATAN
|
||||||
|
|
||||||
|
Buat 4 card horizontal di bagian atas yang menampilkan kegiatan desa.
|
||||||
|
|
||||||
|
Setiap card memiliki:
|
||||||
|
|
||||||
|
* header biru
|
||||||
|
* progress bar kegiatan
|
||||||
|
* tanggal kegiatan
|
||||||
|
* badge status
|
||||||
|
|
||||||
|
Data card:
|
||||||
|
|
||||||
|
1.
|
||||||
|
|
||||||
|
Judul: Rakor 2025
|
||||||
|
Tanggal: 3 Juli 2025
|
||||||
|
Progress: 90%
|
||||||
|
Status: selesai
|
||||||
|
|
||||||
|
2.
|
||||||
|
|
||||||
|
Judul: Pemutakhiran Indeks Desa
|
||||||
|
Tanggal: 3 Juli 2025
|
||||||
|
Progress: 85%
|
||||||
|
Status: selesai
|
||||||
|
|
||||||
|
3.
|
||||||
|
|
||||||
|
Judul: Mengurus Akta Cerai Warga
|
||||||
|
Tanggal: 3 Juli 2025
|
||||||
|
Progress: 80%
|
||||||
|
Status: selesai
|
||||||
|
|
||||||
|
4.
|
||||||
|
|
||||||
|
Judul: Pasek 7 Desa Adat
|
||||||
|
Tanggal: 3 Juli 2025
|
||||||
|
Progress: 92%
|
||||||
|
Status: selesai
|
||||||
|
|
||||||
|
Progress bar:
|
||||||
|
|
||||||
|
* rounded
|
||||||
|
* warna warning
|
||||||
|
* animasi smooth
|
||||||
|
|
||||||
|
Status badge:
|
||||||
|
|
||||||
|
* success color
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
SECTION 2 — GRID DASHBOARD
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
|
||||||
|
3 column grid.
|
||||||
|
|
||||||
|
Left column (sidebar style):
|
||||||
|
Divisi Teraktif
|
||||||
|
|
||||||
|
List item card dengan arrow icon.
|
||||||
|
|
||||||
|
Data:
|
||||||
|
|
||||||
|
Kesejahteraan — 37 kegiatan
|
||||||
|
Pemerintahan — 26 kegiatan
|
||||||
|
Keuangan — 17 kegiatan
|
||||||
|
Sekretaris Desa — 15 kegiatan
|
||||||
|
Tata Usaha TK — 14 kegiatan
|
||||||
|
Perangkat Kewilayahan — 12 kegiatan
|
||||||
|
Pelayanan — 10 kegiatan
|
||||||
|
Perencanaan — 9 kegiatan
|
||||||
|
Tata Usaha & Umum — 7 kegiatan
|
||||||
|
|
||||||
|
Setiap item:
|
||||||
|
|
||||||
|
* rounded
|
||||||
|
* hover effect
|
||||||
|
* arrow icon kanan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Middle column:
|
||||||
|
|
||||||
|
Jumlah Dokumen
|
||||||
|
|
||||||
|
Gunakan **Bar Chart**.
|
||||||
|
|
||||||
|
Kategori:
|
||||||
|
|
||||||
|
* Gambar
|
||||||
|
* Dokumen
|
||||||
|
|
||||||
|
Nilai:
|
||||||
|
|
||||||
|
* Gambar: 300
|
||||||
|
* Dokumen: 310
|
||||||
|
|
||||||
|
Gunakan:
|
||||||
|
Recharts atau Mantine Charts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Right column:
|
||||||
|
|
||||||
|
Progres Kegiatan
|
||||||
|
|
||||||
|
Gunakan **Pie Chart**.
|
||||||
|
|
||||||
|
Data:
|
||||||
|
|
||||||
|
Selesai — 83.33%
|
||||||
|
Dikerjakan — 16.67%
|
||||||
|
Segera Dikerjakan — 0%
|
||||||
|
Dibatalkan — 0%
|
||||||
|
|
||||||
|
Legend harus berwarna.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
SECTION 3 — DISCUSSION PANEL
|
||||||
|
|
||||||
|
Judul: Diskusi
|
||||||
|
|
||||||
|
Tampilkan list diskusi internal staf.
|
||||||
|
|
||||||
|
Item card memiliki:
|
||||||
|
|
||||||
|
* icon chat
|
||||||
|
* judul pesan
|
||||||
|
* nama pengirim
|
||||||
|
* tanggal
|
||||||
|
|
||||||
|
Contoh data:
|
||||||
|
|
||||||
|
"Kepada Pelayanan, mohon di cek..."
|
||||||
|
Pengirim: I.B Surya Prabhawa Manu
|
||||||
|
Tanggal: 12 Apr 2025
|
||||||
|
|
||||||
|
"Kepada staf perencanaan @suar..."
|
||||||
|
Pengirim: Ni Nyoman Yuliani
|
||||||
|
Tanggal: 14 Jun 2025
|
||||||
|
|
||||||
|
"ijin atau mohon kepada KBD sar..."
|
||||||
|
Pengirim: Ni Wayan Martini
|
||||||
|
Tanggal: 12 Apr 2025
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
SECTION 4 — ACARA HARI INI
|
||||||
|
|
||||||
|
Card sederhana.
|
||||||
|
|
||||||
|
Jika tidak ada acara tampilkan:
|
||||||
|
|
||||||
|
"Tidak ada acara hari ini"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
SECTION 5 — ARSIP DIGITAL PERANGKAT DESA
|
||||||
|
|
||||||
|
Grid 2 column.
|
||||||
|
|
||||||
|
Menu arsip:
|
||||||
|
|
||||||
|
Surat Keputusan
|
||||||
|
Dokumentasi
|
||||||
|
Laporan Keuangan
|
||||||
|
Notulensi Rapat
|
||||||
|
|
||||||
|
Setiap item berupa card clickable dengan:
|
||||||
|
|
||||||
|
* icon dokumen
|
||||||
|
* border
|
||||||
|
* hover effect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
DESIGN STYLE
|
||||||
|
|
||||||
|
Gunakan gaya:
|
||||||
|
|
||||||
|
Modern Government Dashboard
|
||||||
|
Clean UI
|
||||||
|
Soft shadow
|
||||||
|
Rounded-xl
|
||||||
|
Spacing besar
|
||||||
|
Minimalistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
RESPONSIVE RULES
|
||||||
|
|
||||||
|
Desktop:
|
||||||
|
12 column grid
|
||||||
|
|
||||||
|
Tablet:
|
||||||
|
6 column grid
|
||||||
|
|
||||||
|
Mobile:
|
||||||
|
single column stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
COMPONENT STRUCTURE
|
||||||
|
|
||||||
|
src/components/dashboard
|
||||||
|
|
||||||
|
activity-card.tsx
|
||||||
|
division-list.tsx
|
||||||
|
document-chart.tsx
|
||||||
|
progress-chart.tsx
|
||||||
|
discussion-panel.tsx
|
||||||
|
event-card.tsx
|
||||||
|
archive-card.tsx
|
||||||
|
|
||||||
|
src/pages
|
||||||
|
|
||||||
|
dashboard.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
CODE QUALITY
|
||||||
|
|
||||||
|
Gunakan:
|
||||||
|
|
||||||
|
* React hooks
|
||||||
|
* reusable components
|
||||||
|
* Mantine components jika perlu
|
||||||
|
* Tailwind utility classes
|
||||||
|
* dark mode support
|
||||||
|
* responsive layout
|
||||||
|
* clean TypeScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
* Halaman dashboard lengkap
|
||||||
|
* Semua komponen reusable
|
||||||
|
* Chart sudah bekerja
|
||||||
|
* Layout identik dengan desain dashboard modern pemerintahan
|
||||||
@@ -386,4 +386,4 @@ const BumdesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BumdesPage;
|
export default BumdesPage;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Grid, Stack, useMantineColorScheme } from "@mantine/core";
|
import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
|
||||||
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
||||||
import { ActivityList } from "./dashboard/activity-list";
|
import { ActivityList } from "./dashboard/activity-list";
|
||||||
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
||||||
@@ -8,149 +8,26 @@ import { SatisfactionChart } from "./dashboard/satisfaction-chart";
|
|||||||
import { SDGSCard } from "./dashboard/sdgs-card";
|
import { SDGSCard } from "./dashboard/sdgs-card";
|
||||||
import { StatCard } from "./dashboard/stat-card";
|
import { StatCard } from "./dashboard/stat-card";
|
||||||
|
|
||||||
// SDGs Icons
|
|
||||||
function EnergyIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M24 4L14 24H22L20 44L34 20H26L24 4Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PeaceIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<circle cx="24" cy="24" r="20" stroke="currentColor" strokeWidth="2" />
|
|
||||||
<path
|
|
||||||
d="M24 4V44M24 24L10 38M24 24L38 38"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HealthIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M24 44C24 44 6 28 6 18C6 11.373 11.373 6 18 6C21.5 6 24.5 7.5 24 12C23.5 7.5 26.5 6 30 6C36.627 6 42 11.373 42 18C42 28 24 44 24 44Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PovertyIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<rect x="6" y="18" width="36" height="26" rx="2" fill="currentColor" />
|
|
||||||
<path
|
|
||||||
d="M14 18V12C14 8.686 16.686 6 20 6H28C31.314 6 34 8.686 34 12V18"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OceanIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6 30C6 30 10 26 14 30C18 34 22 30 26 30C30 30 34 34 38 30C42 26 46 30 46 30"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M6 38C6 38 10 34 14 38C18 42 22 38 26 38C30 38 34 42 38 38C42 34 46 38 46 38"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<circle cx="24" cy="16" r="6" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sdgsData = [
|
const sdgsData = [
|
||||||
{
|
{
|
||||||
title: "Desa Berenergi Bersih dan Terbarukan",
|
title: "Desa Berenergi Bersih dan Terbarukan",
|
||||||
score: 99.64,
|
score: 99.64,
|
||||||
icon: <EnergyIcon />,
|
image: "SDGS-7.png",
|
||||||
color: "#FACC15",
|
|
||||||
bgColor: "#FEF9C3",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Desa Damai Berkeadilan",
|
title: "Desa Damai Berkeadilan",
|
||||||
score: 78.65,
|
score: 78.65,
|
||||||
icon: <PeaceIcon />,
|
image: "SDGS-16.png",
|
||||||
color: "#3B82F6",
|
|
||||||
bgColor: "#DBEAFE",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Desa Sehat dan Sejahtera",
|
title: "Desa Sehat dan Sejahtera",
|
||||||
score: 77.37,
|
score: 77.37,
|
||||||
icon: <HealthIcon />,
|
image: "SDGS-3.png",
|
||||||
color: "#22C55E",
|
|
||||||
bgColor: "#DCFCE7",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Desa Tanpa Kemiskinan",
|
title: "Desa Tanpa Kemiskinan",
|
||||||
score: 52.62,
|
score: 52.62,
|
||||||
icon: <PovertyIcon />,
|
image: "SDGS-1.png",
|
||||||
color: "#EF4444",
|
|
||||||
bgColor: "#FEE2E2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Desa Peduli Lingkungan Laut",
|
|
||||||
score: 50.0,
|
|
||||||
icon: <OceanIcon />,
|
|
||||||
color: "#06B6D4",
|
|
||||||
bgColor: "#CFFAFE",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -202,37 +79,35 @@ export function DashboardContent() {
|
|||||||
|
|
||||||
{/* Section 2: Chart & Division Progress */}
|
{/* Section 2: Chart & Division Progress */}
|
||||||
<Grid gutter="lg">
|
<Grid gutter="lg">
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||||
<ChartSurat />
|
<ChartSurat />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||||
<DivisionProgress />
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Section 3: APBDes Chart */}
|
|
||||||
<ChartAPBDes />
|
|
||||||
|
|
||||||
{/* Section 4 & 5: Activity List & Satisfaction Chart */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
|
||||||
<ActivityList />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
|
||||||
<SatisfactionChart />
|
<SatisfactionChart />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Section 3: APBDes Chart */}
|
||||||
|
<Grid gutter="lg">
|
||||||
|
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||||
|
<DivisionProgress />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||||
|
<ActivityList />
|
||||||
|
{/* <SatisfactionChart /> */}
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ChartAPBDes />
|
||||||
|
|
||||||
{/* Section 6: SDGs Desa Cards */}
|
{/* Section 6: SDGs Desa Cards */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{sdgsData.map((sdg, index) => (
|
{sdgsData.map((sdg, index) => (
|
||||||
<Grid.Col key={index} span={{ base: 12, sm: 6, md: 4, lg: 2.4 }}>
|
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
|
||||||
<SDGSCard
|
<SDGSCard
|
||||||
|
image={<Image src={sdg.image} alt={sdg.title} />}
|
||||||
title={sdg.title}
|
title={sdg.title}
|
||||||
score={sdg.score}
|
score={sdg.score}
|
||||||
icon={sdg.icon}
|
|
||||||
color={sdg.color}
|
|
||||||
bgColor={sdg.bgColor}
|
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,18 +4,10 @@ import type { ReactNode } from "react";
|
|||||||
interface SDGSCardProps {
|
interface SDGSCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
score: number;
|
score: number;
|
||||||
icon: ReactNode;
|
image: ReactNode;
|
||||||
color: string;
|
|
||||||
bgColor: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SDGSCard({
|
export function SDGSCard({ title, score, image }: SDGSCardProps) {
|
||||||
title,
|
|
||||||
score,
|
|
||||||
icon,
|
|
||||||
color,
|
|
||||||
bgColor,
|
|
||||||
}: SDGSCardProps) {
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
@@ -24,29 +16,28 @@ export function SDGSCard({
|
|||||||
p="md"
|
p="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={bgColor}
|
|
||||||
style={{
|
style={{
|
||||||
borderColor: dark ? "#334155" : bgColor,
|
borderColor: dark ? "#334155" : "white",
|
||||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="flex-start" w="100%">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
|
<Box>{image}</Box>
|
||||||
<Box style={{ flex: 1 }}>
|
<Box style={{ flex: 1 }}>
|
||||||
<Text size="sm" c={dark ? "white" : "gray.8"} fw={500} mb="xs">
|
<Text
|
||||||
|
ta={"center"}
|
||||||
|
size="sm"
|
||||||
|
c={dark ? "white" : "gray.8"}
|
||||||
|
fw={500}
|
||||||
|
mb="xs"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xl" fw={700} c={color}>
|
<Text ta={"center"} size="xl" c={dark ? "white" : "gray.8"} fw={700}>
|
||||||
{score.toFixed(2)}
|
{score.toFixed(2)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
color,
|
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Box>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -408,4 +408,4 @@ const DemografiPekerjaan = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DemografiPekerjaan;
|
export default DemografiPekerjaan;
|
||||||
|
|||||||
@@ -118,4 +118,4 @@ export function Header() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,4 +432,4 @@ const HelpPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HelpPage;
|
export default HelpPage;
|
||||||
|
|||||||
@@ -280,4 +280,4 @@ const JennaAnalytic = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default JennaAnalytic;
|
export default JennaAnalytic;
|
||||||
|
|||||||
@@ -322,4 +322,4 @@ const KeamananPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default KeamananPage;
|
export default KeamananPage;
|
||||||
|
|||||||
@@ -354,4 +354,4 @@ const KeuanganAnggaran = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default KeuanganAnggaran;
|
export default KeuanganAnggaran;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Grid, Stack } from "@mantine/core";
|
import { Grid, Stack } from "@mantine/core";
|
||||||
import {
|
import { ActivityCard, } from "./kinerja-divisi/activity-card";
|
||||||
ActivityCard,
|
import { DivisionList } from "./kinerja-divisi/division-list";
|
||||||
ArchiveCard,
|
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
||||||
DiscussionPanel,
|
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
||||||
DivisionList,
|
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
||||||
DocumentChart,
|
import { EventCard } from "./kinerja-divisi/event-card";
|
||||||
EventCard,
|
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||||
ProgressChart,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
// Data for program kegiatan (Section 1)
|
// Data for program kegiatan (Section 1)
|
||||||
const programKegiatanData = [
|
const programKegiatanData = [
|
||||||
|
|||||||
@@ -840,4 +840,4 @@ const PengaduanLayananPublik = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PengaduanLayananPublik;
|
export default PengaduanLayananPublik;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Collapse,
|
Collapse,
|
||||||
Group,
|
Group,
|
||||||
|
Image,
|
||||||
Input,
|
Input,
|
||||||
NavLink as MantineNavLink,
|
NavLink as MantineNavLink,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -60,30 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<Box className={className}>
|
<Box className={className}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Box
|
<Image src="/logo-desa-plus.png" alt="Logo" />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
@@ -204,4 +182,4 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,4 +462,4 @@ const SosialPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SosialPage;
|
export default SosialPage;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|||||||
410
src/index.ts
410
src/index.ts
@@ -12,240 +12,240 @@ const isProduction = process.env.NODE_ENV === "production";
|
|||||||
|
|
||||||
// Auto-seed database in production (ensure admin user exists)
|
// Auto-seed database in production (ensure admin user exists)
|
||||||
if (isProduction && process.env.ADMIN_EMAIL) {
|
if (isProduction && process.env.ADMIN_EMAIL) {
|
||||||
try {
|
try {
|
||||||
console.log("🌱 Running database seed in production...");
|
console.log("🌱 Running database seed in production...");
|
||||||
const { runSeed } = await import("../prisma/seed.ts");
|
const { runSeed } = await import("../prisma/seed.ts");
|
||||||
await runSeed();
|
await runSeed();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("⚠️ Production seed failed:", error);
|
console.error("⚠️ Production seed failed:", error);
|
||||||
// Don't crash the server if seed fails
|
// Don't crash the server if seed fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new Elysia().use(api);
|
const app = new Elysia().use(api);
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
// Development: Use Vite middleware
|
// Development: Use Vite middleware
|
||||||
const { createVite } = await import("./vite");
|
const { createVite } = await import("./vite");
|
||||||
const vite = await createVite();
|
const vite = await createVite();
|
||||||
|
|
||||||
// Serve PWA/TWA assets in dev (root and nested path support)
|
// Serve PWA/TWA assets in dev (root and nested path support)
|
||||||
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
|
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
|
||||||
|
|
||||||
app.post("/__open-in-editor", ({ body }) => {
|
app.post("/__open-in-editor", ({ body }) => {
|
||||||
const { relativePath, lineNumber, columnNumber } = body as {
|
const { relativePath, lineNumber, columnNumber } = body as {
|
||||||
relativePath: string;
|
relativePath: string;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
columnNumber: number;
|
columnNumber: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
openInEditor(relativePath, {
|
openInEditor(relativePath, {
|
||||||
line: lineNumber,
|
line: lineNumber,
|
||||||
column: columnNumber,
|
column: columnNumber,
|
||||||
editor: "antigravity",
|
editor: "antigravity",
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vite middleware for other requests
|
// Vite middleware for other requests
|
||||||
app.all("*", async ({ request }) => {
|
app.all("*", async ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
// Serve transformed index.html for root or any path that should be handled by the SPA
|
// Serve transformed index.html for root or any path that should be handled by the SPA
|
||||||
if (
|
if (
|
||||||
pathname === "/" ||
|
pathname === "/" ||
|
||||||
(!pathname.includes(".") &&
|
(!pathname.includes(".") &&
|
||||||
!pathname.startsWith("/@") &&
|
!pathname.startsWith("/@") &&
|
||||||
!pathname.startsWith("/inspector") &&
|
!pathname.startsWith("/inspector") &&
|
||||||
!pathname.startsWith("/__open-stack-frame-in-editor") &&
|
!pathname.startsWith("/__open-stack-frame-in-editor") &&
|
||||||
!pathname.startsWith("/api"))
|
!pathname.startsWith("/api"))
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const htmlPath = path.resolve("src/index.html");
|
const htmlPath = path.resolve("src/index.html");
|
||||||
let html = fs.readFileSync(htmlPath, "utf-8");
|
let html = fs.readFileSync(htmlPath, "utf-8");
|
||||||
html = await vite.transformIndexHtml(pathname, html);
|
html = await vite.transformIndexHtml(pathname, html);
|
||||||
|
|
||||||
return new Response(html, {
|
return new Response(html, {
|
||||||
headers: { "Content-Type": "text/html" },
|
headers: { "Content-Type": "text/html" },
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<Response>((resolve) => {
|
return new Promise<Response>((resolve) => {
|
||||||
// Use a Proxy to mock Node.js req because Bun's Request is read-only
|
// Use a Proxy to mock Node.js req because Bun's Request is read-only
|
||||||
const req = new Proxy(request, {
|
const req = new Proxy(request, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (prop === "url") return pathname + url.search;
|
if (prop === "url") return pathname + url.search;
|
||||||
if (prop === "method") return request.method;
|
if (prop === "method") return request.method;
|
||||||
if (prop === "headers")
|
if (prop === "headers")
|
||||||
return Object.fromEntries(request.headers as any);
|
return Object.fromEntries(request.headers as any);
|
||||||
return (target as any)[prop];
|
return (target as any)[prop];
|
||||||
},
|
},
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
setHeader(name: string, value: string) {
|
setHeader(name: string, value: string) {
|
||||||
this.headers[name.toLowerCase()] = value;
|
this.headers[name.toLowerCase()] = value;
|
||||||
},
|
},
|
||||||
getHeader(name: string) {
|
getHeader(name: string) {
|
||||||
return this.headers[name.toLowerCase()];
|
return this.headers[name.toLowerCase()];
|
||||||
},
|
},
|
||||||
writeHead(code: number, headers: Record<string, string>) {
|
writeHead(code: number, headers: Record<string, string>) {
|
||||||
this.statusCode = code;
|
this.statusCode = code;
|
||||||
Object.assign(this.headers, headers);
|
Object.assign(this.headers, headers);
|
||||||
},
|
},
|
||||||
write(chunk: any, callback?: () => void) {
|
write(chunk: any, callback?: () => void) {
|
||||||
// Collect chunks for streaming responses
|
// Collect chunks for streaming responses
|
||||||
if (!this._chunks) this._chunks = [];
|
if (!this._chunks) this._chunks = [];
|
||||||
this._chunks.push(chunk);
|
this._chunks.push(chunk);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
return true; // Indicate we can accept more data
|
return true; // Indicate we can accept more data
|
||||||
},
|
},
|
||||||
headers: {} as Record<string, string>,
|
headers: {} as Record<string, string>,
|
||||||
end(data: any) {
|
end(data: any) {
|
||||||
// Handle potential Buffer or string data from Vite
|
// Handle potential Buffer or string data from Vite
|
||||||
let body = data;
|
let body = data;
|
||||||
// If we have collected chunks from write() calls, combine them
|
// If we have collected chunks from write() calls, combine them
|
||||||
if (this._chunks && this._chunks.length > 0) {
|
if (this._chunks && this._chunks.length > 0) {
|
||||||
body = Buffer.concat(this._chunks);
|
body = Buffer.concat(this._chunks);
|
||||||
}
|
}
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
body = data;
|
body = data;
|
||||||
} else if (typeof data === "string") {
|
} else if (typeof data === "string") {
|
||||||
body = data;
|
body = data;
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
body = String(data);
|
body = String(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
new Response(body || "", {
|
new Response(body || "", {
|
||||||
status: this.statusCode,
|
status: this.statusCode,
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// Minimal event emitter mock
|
// Minimal event emitter mock
|
||||||
once() {
|
once() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
on() {
|
on() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
emit() {
|
emit() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
removeListener() {
|
removeListener() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
vite.middlewares(req, res, (err: any) => {
|
vite.middlewares(req, res, (err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Vite middleware error:", err);
|
console.error("Vite middleware error:", err);
|
||||||
resolve(new Response(err.stack || err.toString(), { status: 500 }));
|
resolve(new Response(err.stack || err.toString(), { status: 500 }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If Vite doesn't handle it, return 404
|
// If Vite doesn't handle it, return 404
|
||||||
resolve(new Response("Not Found", { status: 404 }));
|
resolve(new Response("Not Found", { status: 404 }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Production: Final catch-all for static files and SPA fallback
|
// Production: Final catch-all for static files and SPA fallback
|
||||||
app.all("*", async ({ request }) => {
|
app.all("*", async ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
// 1. Try exact match in dist
|
// 1. Try exact match in dist
|
||||||
let filePath = path.join(
|
let filePath = path.join(
|
||||||
"dist",
|
"dist",
|
||||||
pathname === "/" ? "index.html" : pathname,
|
pathname === "/" ? "index.html" : pathname,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
|
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
const srcPath = path.join("src", pathname);
|
const srcPath = path.join("src", pathname);
|
||||||
if (fs.existsSync(srcPath)) {
|
if (fs.existsSync(srcPath)) {
|
||||||
filePath = srcPath;
|
filePath = srcPath;
|
||||||
}
|
}
|
||||||
// Check public folder for static assets
|
// Check public folder for static assets
|
||||||
const publicPath = path.join("public", pathname);
|
const publicPath = path.join("public", pathname);
|
||||||
if (fs.existsSync(publicPath)) {
|
if (fs.existsSync(publicPath)) {
|
||||||
filePath = publicPath;
|
filePath = publicPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
// 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 (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||||
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
||||||
const filename = path.basename(pathname);
|
const filename = path.basename(pathname);
|
||||||
|
|
||||||
// Try root of dist
|
// Try root of dist
|
||||||
const fallbackDistPath = path.join("dist", filename);
|
const fallbackDistPath = path.join("dist", filename);
|
||||||
if (
|
if (
|
||||||
fs.existsSync(fallbackDistPath) &&
|
fs.existsSync(fallbackDistPath) &&
|
||||||
fs.statSync(fallbackDistPath).isFile()
|
fs.statSync(fallbackDistPath).isFile()
|
||||||
) {
|
) {
|
||||||
filePath = fallbackDistPath;
|
filePath = fallbackDistPath;
|
||||||
}
|
}
|
||||||
// Try public folder
|
// Try public folder
|
||||||
else {
|
else {
|
||||||
const fallbackPublicPath = path.join("public", filename);
|
const fallbackPublicPath = path.join("public", filename);
|
||||||
if (
|
if (
|
||||||
fs.existsSync(fallbackPublicPath) &&
|
fs.existsSync(fallbackPublicPath) &&
|
||||||
fs.statSync(fallbackPublicPath).isFile()
|
fs.statSync(fallbackPublicPath).isFile()
|
||||||
) {
|
) {
|
||||||
filePath = fallbackPublicPath;
|
filePath = fallbackPublicPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Special handling for PWA files in src
|
// Special handling for PWA files in src
|
||||||
if (pathname.includes("assetlinks.json")) {
|
if (pathname.includes("assetlinks.json")) {
|
||||||
const srcFilename = pathname.includes("assetlinks.json")
|
const srcFilename = pathname.includes("assetlinks.json")
|
||||||
? ".well-known/assetlinks.json"
|
? ".well-known/assetlinks.json"
|
||||||
: filename;
|
: filename;
|
||||||
const fallbackSrcPath = path.join("src", srcFilename);
|
const fallbackSrcPath = path.join("src", srcFilename);
|
||||||
if (
|
if (
|
||||||
fs.existsSync(fallbackSrcPath) &&
|
fs.existsSync(fallbackSrcPath) &&
|
||||||
fs.statSync(fallbackSrcPath).isFile()
|
fs.statSync(fallbackSrcPath).isFile()
|
||||||
) {
|
) {
|
||||||
filePath = fallbackSrcPath;
|
filePath = fallbackSrcPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||||
const file = Bun.file(filePath);
|
const file = Bun.file(filePath);
|
||||||
return new Response(file, {
|
return new Response(file, {
|
||||||
headers: {
|
headers: {
|
||||||
Vary: "Accept-Encoding",
|
Vary: "Accept-Encoding",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. SPA Fallback: Serve index.html
|
// 3. SPA Fallback: Serve index.html
|
||||||
const indexHtml = path.join("dist", "index.html");
|
const indexHtml = path.join("dist", "index.html");
|
||||||
if (fs.existsSync(indexHtml)) {
|
if (fs.existsSync(indexHtml)) {
|
||||||
return new Response(Bun.file(indexHtml), {
|
return new Response(Bun.file(indexHtml), {
|
||||||
headers: {
|
headers: {
|
||||||
Vary: "Accept-Encoding",
|
Vary: "Accept-Encoding",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response("Not Found", { status: 404 });
|
return new Response("Not Found", { status: 404 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(PORT);
|
app.listen(PORT);
|
||||||
|
|
||||||
console.log(
|
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;
|
export type ApiApp = typeof app;
|
||||||
|
|||||||
@@ -152,4 +152,4 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
|||||||
* Default Middleware Export
|
* Default Middleware Export
|
||||||
* ================================ */
|
* ================================ */
|
||||||
|
|
||||||
export const protectedRouteMiddleware = createProtectedRoute();
|
export const protectedRouteMiddleware = createProtectedRoute();
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ export const Route = createRootRoute({
|
|||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ function DashboardPage() {
|
|||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import {
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
AppShell,
|
|
||||||
Burger,
|
|
||||||
Group,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
||||||
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
Outlet,
|
||||||
|
useRouterState,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
@@ -44,12 +43,7 @@ function PengaturanLayout() {
|
|||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
||||||
<Burger
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||||
opened={opened}
|
|
||||||
onClick={toggle}
|
|
||||||
hiddenFrom="sm"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Header />
|
<Header />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VITE_PUBLIC_URL = (() => {
|
export const VITE_PUBLIC_URL = (() => {
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1. BETTER_AUTH_URL (standard for better-auth)
|
// 1. BETTER_AUTH_URL (standard for better-auth)
|
||||||
// 2. VITE_PUBLIC_URL (our app standard)
|
// 2. VITE_PUBLIC_URL (our app standard)
|
||||||
// 3. window.location.origin (browser fallback)
|
// 3. window.location.origin (browser fallback)
|
||||||
|
|||||||
Reference in New Issue
Block a user