Compare commits

..

3 Commits

Author SHA1 Message Date
b1d28a8322 fix-admin-menu-desa-profile 2026-02-25 15:25:51 +08:00
b86a3a85c3 fix: force default light mode for public pages and admin
- Set defaultColorScheme='light' in root MantineProvider
- Change darkModeStore default from system preference to false (light)
- Add MantineProvider with light theme to darmasaba/layout.tsx
- Remove dark mode dependency from ModuleView component
- Prevent system color scheme from affecting initial page load

This ensures consistent light mode on first visit for both
public pages and admin panel, regardless of OS settings.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:45:27 +08:00
fd63bb0fd4 feat: implement dark mode support & fix Prisma schema validation
- Add dark mode toggle component in admin header
- Integrate dark mode store across admin layout and components
- Add unified typography and surface components for consistent theming
- Implement smooth transitions for dark/light mode switching
- Fix Prisma schema: remove @default(null) from DateTime? fields
- Update form validation for inovasi, lingkungan, and pendidikan modules
- Add form validation and improve UX across multiple admin pages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:41:48 +08:00
13 changed files with 259 additions and 53 deletions

View File

@@ -236,7 +236,7 @@ model PrestasiDesa {
imageId String? imageId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -245,7 +245,7 @@ model KategoriPrestasiDesa {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
PrestasiDesa PrestasiDesa[] PrestasiDesa PrestasiDesa[]
} }
@@ -263,7 +263,7 @@ model Responden {
kelompokUmurId String kelompokUmurId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -272,7 +272,7 @@ model JenisKelaminResponden {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
Responden Responden[] Responden Responden[]
} }
@@ -282,7 +282,7 @@ model PilihanRatingResponden {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
Responden Responden[] Responden Responden[]
} }
@@ -292,7 +292,7 @@ model UmurResponden {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
Responden Responden[] Responden Responden[]
} }
@@ -326,6 +326,7 @@ model PosisiOrganisasiPPID {
isActive Boolean @default(true) isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime?
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent") children PosisiOrganisasiPPID[] @relation("Parent")
StrukturOrganisasiPPID StrukturOrganisasiPPID[] StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@ -345,6 +346,7 @@ model PegawaiPPID {
isActive Boolean @default(true) isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime?
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id]) posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik strukturOrganisasi StrukturPPID[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[] StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@ -370,7 +372,7 @@ model VisiMisiPPID {
misi String @db.Text misi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -381,7 +383,7 @@ model DasarHukumPPID {
content String @db.Text content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -398,7 +400,7 @@ model ProfilePPID {
imageId String? imageId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -410,7 +412,7 @@ model DaftarInformasiPublik {
tanggal DateTime @db.Date tanggal DateTime @db.Date
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -431,7 +433,7 @@ model PermohonanInformasiPublik {
caraMemperolehSalinanInformasiId String? caraMemperolehSalinanInformasiId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -440,7 +442,7 @@ model JenisInformasiDiminta {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[] PermohonanInformasiPublik PermohonanInformasiPublik[]
} }
@@ -450,7 +452,7 @@ model CaraMemperolehInformasi {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[] PermohonanInformasiPublik PermohonanInformasiPublik[]
} }
@@ -460,7 +462,7 @@ model CaraMemperolehSalinanInformasi {
name String @unique name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[] PermohonanInformasiPublik PermohonanInformasiPublik[]
} }
@@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan {
alasan String alasan String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -531,7 +533,7 @@ model SejarahDesa {
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -541,7 +543,7 @@ model VisiMisiDesa {
misi String @db.Text misi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -551,7 +553,7 @@ model LambangDesa {
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
@@ -562,7 +564,7 @@ model MaskotDesa {
images ProfileDesaImage[] images ProfileDesaImage[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }

View File

@@ -69,8 +69,8 @@ import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
(async () => { (async () => {
// Always run seedAssets to handle new images without duplication // Always run seedAssets to handle new images without duplication
// console.log("📂 Checking for new assets to seed..."); console.log("📂 Checking for new assets to seed...");
// await seedAssets(); await seedAssets();
// // =========== FILE STORAGE =========== // // =========== FILE STORAGE ===========

View File

@@ -95,7 +95,7 @@ function Page() {
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.4, md: 1.4 }} lh={{ base: 1.4, md: 1.4 }}
> >
I.B. Surya Prabhawa Manuaba, S.H., M.H. {perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."}
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>

View File

@@ -354,7 +354,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
borderLeft: `2px solid ${tokens.colors.primary}`, borderLeft: `2px solid ${tokens.colors.primary}`,
}), }),
...(mounted && isChildActive && !isDark && { ...(mounted && isChildActive && !isDark && {
backgroundColor: tokens.colors.bg.hover, backgroundColor: 'rgba(25, 113, 194, 0.1)',
borderLeft: `2px solid ${tokens.colors.primary}`,
}), }),
} }
}} }}

View File

@@ -0,0 +1,40 @@
import prisma from "@/lib/prisma";
import { requireAuth } from "@/lib/api-auth";
export default async function sejarahDesaFindFirst(request: Request) {
// ✅ Authentication check
const headers = new Headers(request.url);
const authResult = await requireAuth({ headers });
if (!authResult.authenticated) {
return authResult.response;
}
try {
// Get the first active record
const data = await prisma.sejarahDesa.findFirst({
where: {
isActive: true,
deletedAt: null
},
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, {status: 404})
}
return Response.json({
success: true,
data,
}, {status: 200})
} catch (error) {
console.error("Gagal mengambil data sejarah desa:", error)
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, {status: 500})
}
}

View File

@@ -1,11 +1,16 @@
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import sejarahDesaFindById from "./find-by-id"; import sejarahDesaFindById from "./find-by-id";
import sejarahDesaUpdate from "./update"; import sejarahDesaUpdate from "./update";
import sejarahDesaFindFirst from "./find-first";
const SejarahDesa = new Elysia({ const SejarahDesa = new Elysia({
prefix: "/sejarah", prefix: "/sejarah",
tags: ["Desa/Profile"], tags: ["Desa/Profile"],
}) })
.get("/first", async (context) => {
const response = await sejarahDesaFindFirst(new Request(context.request));
return response;
})
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await sejarahDesaFindById(new Request(context.request)); const response = await sejarahDesaFindById(new Request(context.request));
return response; return response;

View File

@@ -1,7 +1,14 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { requireAuth } from "@/lib/api-auth";
import { Context } from "elysia"; import { Context } from "elysia";
export default async function sejarahDesaUpdate(context: Context) { export default async function sejarahDesaUpdate(context: Context) {
// ✅ Authentication check
const authResult = await requireAuth(context);
if (!authResult.authenticated) {
return authResult.response;
}
try { try {
const id = context.params?.id as string; const id = context.params?.id as string;
const body = await context.body as { const body = await context.body as {

View File

@@ -10,8 +10,7 @@ import {
SimpleGrid, SimpleGrid,
Skeleton, Skeleton,
Stack, Stack,
Text, Text
useMantineColorScheme
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
@@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
function ModuleItem({ data }: { data: ProgramInovasiItem }) { function ModuleItem({ data }: { data: ProgramInovasiItem }) {
const router = useTransitionRouter(); const router = useTransitionRouter();
const { colorScheme } = useMantineColorScheme();
const isDark = colorScheme === "dark";
return ( return (
<motion.div whileHover={{ scale: 1.03 }}> <motion.div whileHover={{ scale: 1.03 }}>
@@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
role="button" role="button"
tabIndex={0} tabIndex={0}
className="cursor-pointer transition-all" className="cursor-pointer transition-all"
bg={isDark ? "dark.6" : "white"} bg="white"
> >
<Center h={160}> <Center h={160}>
{data.image?.link ? ( {data.image?.link ? (

View File

@@ -1,3 +1,5 @@
"use client";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Box, Space, Stack } from "@mantine/core"; import { Box, Space, Stack } from "@mantine/core";
@@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar";
import Footer from "./_com/Footer"; import Footer from "./_com/Footer";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<Stack gap={0} bg={colors.grey[1]}> <Stack gap={0} bg={colors.grey[1]}>
<Navbar /> <Navbar />
<Space h={{ <Space h={{
base: "3.9rem", base: "3.9rem",
md: "2.5rem" md: "2.5rem"
}} /> }} />
<Box style={{ <Box style={{
overflow: "scroll" overflow: "scroll"
}}> }}>
{children} {children}
</Box> </Box>
<Footer /> <Footer />
</Stack> </Stack>
) )
} }

View File

@@ -98,16 +98,16 @@ export default function RootLayout({
<html lang="id" {...mantineHtmlProps}> <html lang="id" {...mantineHtmlProps}>
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<ColorSchemeScript /> <ColorSchemeScript defaultColorScheme="light" />
</head> </head>
<body> <body>
<MantineProvider theme={theme}> <MantineProvider theme={theme} defaultColorScheme="light">
{children} {children}
<LoadDataFirstClient /> <LoadDataFirstClient />
<ToastContainer <ToastContainer
position="bottom-center" position="bottom-center"
hideProgressBar hideProgressBar
style={{ zIndex: 9999 }} style={{ zIndex: 9999 }}
/> />
</MantineProvider> </MantineProvider>
</body> </body>

84
src/lib/api-auth.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* Authentication helper untuk API endpoints
*
* Usage:
* import { requireAuth } from "@/lib/api-auth";
*
* export default async function myEndpoint(context: Context) {
* const authResult = await requireAuth(context);
* if (!authResult.authenticated) {
* return authResult.response;
* }
* // Lanjut proses dengan authResult.user
* }
*/
import { getSession } from "@/lib/session";
export type AuthResult =
| { authenticated: true; user: any }
| { authenticated: false; response: Response };
export async function requireAuth(context: any): Promise<AuthResult> {
try {
// Cek session dari cookies
const session = await getSession();
if (!session || !session.user) {
return {
authenticated: false,
response: new Response(JSON.stringify({
success: false,
message: "Unauthorized - Silakan login terlebih dahulu"
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
};
}
// Check jika user masih aktif
if (!session.user.isActive) {
return {
authenticated: false,
response: new Response(JSON.stringify({
success: false,
message: "Akun Anda tidak aktif. Hubungi administrator."
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
})
};
}
return {
authenticated: true,
user: session.user
};
} catch (error) {
console.error("Auth error:", error);
return {
authenticated: false,
response: new Response(JSON.stringify({
success: false,
message: "Authentication error"
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
};
}
}
/**
* Optional auth - tidak error jika tidak authenticated
* Berguna untuk endpoint yang bisa diakses public atau private
*/
export async function optionalAuth(context: any): Promise<any> {
try {
const session = await getSession();
return session?.user || null;
} catch (error) {
return null;
}
}

68
src/lib/session.ts Normal file
View File

@@ -0,0 +1,68 @@
/**
* Session helper menggunakan iron-session
*
* Usage:
* import { getSession } from "@/lib/session";
*
* const session = await getSession();
* if (session?.user) {
* // User authenticated
* }
*/
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
export type SessionData = {
user?: {
id: string;
name: string;
roleId: number;
menuIds?: string[] | null;
isActive?: boolean;
};
};
export type Session = SessionData & {
save: () => Promise<void>;
destroy: () => Promise<void>;
};
const SESSION_OPTIONS = {
cookieName: 'desa-session',
password: process.env.SESSION_PASSWORD || 'default-password-change-in-production',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax' as const,
maxAge: 60 * 60 * 24 * 7, // 7 days
},
};
export async function getSession(): Promise<SessionData | null> {
try {
const cookieStore = await cookies();
const session = await getIronSession<SessionData>(
cookieStore,
SESSION_OPTIONS
);
return session;
} catch (error) {
console.error('Session error:', error);
return null;
}
}
export async function destroySession(): Promise<void> {
try {
const cookieStore = await cookies();
const session = await getIronSession<SessionData>(
cookieStore,
SESSION_OPTIONS
);
await session.destroy();
} catch (error) {
console.error('Destroy session error:', error);
}
}

View File

@@ -21,17 +21,18 @@ import { proxy, useSnapshot } from 'valtio';
const STORAGE_KEY = 'darmasaba-admin-dark-mode'; const STORAGE_KEY = 'darmasaba-admin-dark-mode';
// Initialize from localStorage or system preference // Initialize from localStorage or default to light mode
const getInitialDarkMode = (): boolean => { const getInitialDarkMode = (): boolean => {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
if (stored !== null) { if (stored !== null) {
return stored === 'true'; return stored === 'true';
} }
// Fallback to system preference // Default to light mode for first-time users
return window.matchMedia('(prefers-color-scheme: dark)').matches; // System preference is NOT used as default to ensure consistent UX
return false;
}; };
class DarkModeStore { class DarkModeStore {