Compare commits
13 Commits
nico/test-
...
nico/28-no
| Author | SHA1 | Date | |
|---|---|---|---|
| b2066caa13 | |||
| 9bf3ec72cf | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 | |||
| 0018bdc251 | |||
| 83fb39a957 | |||
| 7238692dd0 | |||
| 8b50139d79 | |||
| 066180fc0e | |||
| 67f29aabef |
@@ -19,7 +19,7 @@ import { authStore } from '@/store/authStore';
|
||||
|
||||
export default function Validasi() {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const [nomor, setNomor] = useState<string | null>(null);
|
||||
const [otp, setOtp] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -35,7 +35,7 @@ export default function Validasi() {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
setIsRegistrationFlow(data.flow === 'register');
|
||||
console.log('🔍 Flow detected from cookie:', data.flow);
|
||||
@@ -45,7 +45,7 @@ export default function Validasi() {
|
||||
setIsRegistrationFlow(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
checkFlow();
|
||||
}, []);
|
||||
|
||||
@@ -110,6 +110,7 @@ export default function Validasi() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Verify OTP
|
||||
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -123,19 +124,25 @@ export default function Validasi() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Finalize registration
|
||||
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nomor, username, kodeId }),
|
||||
body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await finalizeRes.json();
|
||||
|
||||
if (data.success || finalizeRes.redirected) {
|
||||
// ✅ Cleanup setelah registrasi sukses
|
||||
|
||||
// ✅ Check JSON response (bukan redirect)
|
||||
if (data.success) {
|
||||
toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
|
||||
await cleanupStorage();
|
||||
window.location.href = '/waiting-room';
|
||||
|
||||
// ✅ Client-side redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = '/waiting-room';
|
||||
}, 1000);
|
||||
} else {
|
||||
toast.error(data.message || 'Registrasi gagal');
|
||||
}
|
||||
@@ -197,7 +204,7 @@ export default function Validasi() {
|
||||
localStorage.removeItem('auth_kodeId');
|
||||
localStorage.removeItem('auth_nomor');
|
||||
localStorage.removeItem('auth_username');
|
||||
|
||||
|
||||
// Clear cookie
|
||||
try {
|
||||
await fetch('/api/auth/clear-flow', {
|
||||
|
||||
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// src/app/admin/_com/getMenuIdsByRoleId.ts
|
||||
import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||
|
||||
/**
|
||||
* Mengembalikan daftar ID menu (string[]) berdasarkan roleId
|
||||
*/
|
||||
export function getMenuIdsByRoleId(roleId: string | number): string[] {
|
||||
const id = typeof roleId === 'string' ? parseInt(roleId, 10) : roleId;
|
||||
|
||||
switch (id) {
|
||||
case 0:
|
||||
// Asumsikan devBar ada dan punya struktur sama
|
||||
return []; // atau sesuaikan jika ada devBar
|
||||
case 1:
|
||||
return navBar.map(section => section.id);
|
||||
case 2:
|
||||
return role1.map(section => section.id);
|
||||
case 3:
|
||||
return role2.map(section => section.id);
|
||||
case 4:
|
||||
return role3.map(section => section.id);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ function ListUser({ search }: { search: string }) {
|
||||
const success = await stateUser.update.submit({
|
||||
id: userId,
|
||||
roleId: newRoleId,
|
||||
|
||||
});
|
||||
|
||||
if (success) {
|
||||
@@ -136,9 +137,10 @@ function ListUser({ search }: { search: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = (data || []).filter(
|
||||
(item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
|
||||
);
|
||||
const filteredData = (data || []).filter((item) => {
|
||||
return item.roleId !== "0" && item.roleId !== "1";
|
||||
});
|
||||
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -183,7 +185,7 @@ function ListUser({ search }: { search: string }) {
|
||||
<Select
|
||||
placeholder="Pilih role"
|
||||
data={stateRole.findMany.data
|
||||
.filter(r => r.id !== "0") // ❌ Sembunyikan SUPERADMIN
|
||||
.filter(r => r.id !== "0" && r.id !== "1") // ❌ Sembunyikan SUPERADMIN dan DEVELOPER
|
||||
.map(r => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
|
||||
@@ -1,3 +1,399 @@
|
||||
// 'use client'
|
||||
|
||||
// import colors from "@/con/colors";
|
||||
// import { authStore } from "@/store/authStore";
|
||||
// import {
|
||||
// ActionIcon,
|
||||
// AppShell,
|
||||
// AppShellHeader,
|
||||
// AppShellMain,
|
||||
// AppShellNavbar,
|
||||
// Burger,
|
||||
// Center,
|
||||
// Flex,
|
||||
// Group,
|
||||
// Image,
|
||||
// Loader,
|
||||
// NavLink,
|
||||
// ScrollArea,
|
||||
// Text,
|
||||
// Tooltip,
|
||||
// rem
|
||||
// } from "@mantine/core";
|
||||
// import { useDisclosure } from "@mantine/hooks";
|
||||
// import {
|
||||
// IconChevronLeft,
|
||||
// IconChevronRight,
|
||||
// IconLogout2
|
||||
// } from "@tabler/icons-react";
|
||||
// import _ from "lodash";
|
||||
// import Link from "next/link";
|
||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
// import { useEffect, useState } from "react";
|
||||
// // import { useSnapshot } from "valtio";
|
||||
// import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
|
||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
// const [opened, { toggle }] = useDisclosure();
|
||||
// const [loading, setLoading] = useState(true);
|
||||
// const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
// const router = useRouter();
|
||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
// // const { user } = useSnapshot(authStore);
|
||||
|
||||
// // console.log("Current user in store:", user);
|
||||
|
||||
// // ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
||||
// useEffect(() => {
|
||||
// const fetchUser = async () => {
|
||||
// try {
|
||||
// const res = await fetch('/api/auth/me');
|
||||
// const data = await res.json();
|
||||
|
||||
// if (data.user) {
|
||||
// // ✅ Check if user is NOT active → redirect to waiting room
|
||||
// if (!data.user.isActive) {
|
||||
// authStore.setUser(null);
|
||||
// router.replace('/waiting-room');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // ✅ Fetch menuIds
|
||||
// const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||
// const menuData = await menuRes.json();
|
||||
|
||||
// const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||
// ? [...menuData.menuIds]
|
||||
// : null;
|
||||
|
||||
// // ✅ Set user dengan menuIds yang fresh
|
||||
// authStore.setUser({
|
||||
// id: data.user.id,
|
||||
// name: data.user.name,
|
||||
// roleId: Number(data.user.roleId),
|
||||
// menuIds,
|
||||
// isActive: data.user.isActive
|
||||
// });
|
||||
|
||||
// // ✅ TAMBAHKAN INI: Redirect ke dashboard sesuai roleId
|
||||
// const currentPath = window.location.pathname;
|
||||
// const expectedPath = getRedirectPath(Number(data.user.roleId));
|
||||
|
||||
// // Jika user di halaman /admin tapi bukan di path yang sesuai roleId
|
||||
// if (currentPath === '/admin' || !currentPath.startsWith(expectedPath)) {
|
||||
// router.replace(expectedPath);
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// authStore.setUser(null);
|
||||
// router.replace('/login');
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Gagal memuat data pengguna:', error);
|
||||
// authStore.setUser(null);
|
||||
// router.replace('/login');
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// fetchUser();
|
||||
// }, [router]);
|
||||
|
||||
// // ✅ Fungsi helper untuk get redirect path
|
||||
// const getRedirectPath = (roleId: number): string => {
|
||||
// switch (roleId) {
|
||||
// case 0: // DEVELOPER
|
||||
// case 1: // SUPERADMIN
|
||||
// case 2: // ADMIN_DESA
|
||||
// return '/admin/landing-page/profil/program-inovasi';
|
||||
// case 3: // ADMIN_KESEHATAN
|
||||
// return '/admin/kesehatan/posyandu';
|
||||
// case 4: // ADMIN_PENDIDIKAN
|
||||
// return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
// default:
|
||||
// return '/admin';
|
||||
// }
|
||||
// };
|
||||
|
||||
// if (loading) {
|
||||
// return (
|
||||
// <AppShell>
|
||||
// <AppShellMain>
|
||||
// <Center h="100vh">
|
||||
// <Loader />
|
||||
// </Center>
|
||||
// </AppShellMain>
|
||||
// </AppShell>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // ✅ Ambil menu berdasarkan roleId dan menuIds
|
||||
// const currentNav = authStore.user
|
||||
// ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||
// : [];
|
||||
|
||||
// const handleLogout = async () => {
|
||||
// try {
|
||||
// setIsLoggingOut(true);
|
||||
|
||||
// // ✅ Panggil API logout untuk clear session di server
|
||||
// const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||
// const result = await response.json();
|
||||
|
||||
// if (result.success) {
|
||||
// // Clear user data dari store
|
||||
// authStore.setUser(null);
|
||||
|
||||
// // Clear localStorage
|
||||
// localStorage.removeItem('auth_nomor');
|
||||
// localStorage.removeItem('auth_kodeId');
|
||||
|
||||
// // Force reload untuk reset semua state
|
||||
// window.location.href = '/login';
|
||||
// } else {
|
||||
// console.error('Logout failed:', result.message);
|
||||
// // Tetap redirect meskipun gagal
|
||||
// authStore.setUser(null);
|
||||
// window.location.href = '/login';
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error during logout:', error);
|
||||
// // Tetap clear store dan redirect jika error
|
||||
// authStore.setUser(null);
|
||||
// window.location.href = '/login';
|
||||
// } finally {
|
||||
// setIsLoggingOut(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <AppShell
|
||||
// suppressHydrationWarning
|
||||
// header={{ height: 64 }}
|
||||
// navbar={{
|
||||
// width: { base: 260, sm: 280, lg: 300 },
|
||||
// breakpoint: 'sm',
|
||||
// collapsed: {
|
||||
// mobile: !opened,
|
||||
// desktop: !desktopOpened,
|
||||
// },
|
||||
// }}
|
||||
// padding="md"
|
||||
// >
|
||||
// <AppShellHeader
|
||||
// style={{
|
||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
||||
// padding: '0 16px',
|
||||
// }}
|
||||
// px={{ base: 'sm', sm: 'md' }}
|
||||
// py={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
||||
// <Flex align="center" gap="sm">
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={{ base: 32, sm: 40 }}
|
||||
// h={{ base: 32, sm: 40 }}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '32px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// <Text
|
||||
// fw={700}
|
||||
// c={colors["blue-button"]}
|
||||
// fz={{ base: 'md', sm: 'xl' }}
|
||||
// >
|
||||
// Admin Darmasaba
|
||||
// </Text>
|
||||
// </Flex>
|
||||
|
||||
// <Group gap="xs">
|
||||
// {!desktopOpened && (
|
||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronRight />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// )}
|
||||
|
||||
// <Burger
|
||||
// opened={opened}
|
||||
// onClick={toggle}
|
||||
// hiddenFrom="sm"
|
||||
// size="md"
|
||||
// color={colors["blue-button"]}
|
||||
// mr="xs"
|
||||
// />
|
||||
|
||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={() => {
|
||||
// router.push("/darmasaba");
|
||||
// }}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// >
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={20}
|
||||
// h={20}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '20px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={handleLogout}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// loading={isLoggingOut}
|
||||
// disabled={isLoggingOut}
|
||||
// >
|
||||
// <IconLogout2 size={22} />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </Group>
|
||||
// </AppShellHeader>
|
||||
|
||||
// <AppShellNavbar
|
||||
// component={ScrollArea}
|
||||
// style={{
|
||||
// background: "#ffffff",
|
||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||
// }}
|
||||
// p={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <AppShell.Section p="sm">
|
||||
// {currentNav.map((v, k) => {
|
||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={k}
|
||||
// defaultOpened={isParentActive}
|
||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||
// {v.name}
|
||||
// </Text>
|
||||
// }
|
||||
// style={{
|
||||
// borderRadius: rem(10),
|
||||
// marginBottom: rem(4),
|
||||
// transition: "background 150ms ease",
|
||||
// }}
|
||||
// styles={{
|
||||
// root: {
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// },
|
||||
// }}
|
||||
// variant="light"
|
||||
// active={isParentActive}
|
||||
// >
|
||||
// {v.children.map((child, key) => {
|
||||
// const isChildActive = segments.includes(
|
||||
// _.lowerCase(child.name)
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={key}
|
||||
// href={child.path}
|
||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||
// {child.name}
|
||||
// </Text>
|
||||
// }
|
||||
// styles={{
|
||||
// root: {
|
||||
// borderRadius: rem(8),
|
||||
// marginBottom: rem(2),
|
||||
// transition: 'background 150ms ease',
|
||||
// padding: '6px 12px',
|
||||
// '&:hover': {
|
||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// ...(isChildActive && {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||
// }),
|
||||
// },
|
||||
// }}
|
||||
// active={isChildActive}
|
||||
// component={Link}
|
||||
// />
|
||||
// );
|
||||
// })}
|
||||
// </NavLink>
|
||||
// );
|
||||
// })}
|
||||
// </AppShell.Section>
|
||||
|
||||
// <AppShell.Section py="md">
|
||||
// <Group justify="end" pr="sm">
|
||||
// <Tooltip
|
||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||
// position="top"
|
||||
// withArrow
|
||||
// >
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronLeft />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </AppShell.Section>
|
||||
// </AppShellNavbar>
|
||||
|
||||
// <AppShellMain
|
||||
// style={{
|
||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||
// minHeight: "100vh",
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </AppShellMain>
|
||||
// </AppShell>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
// app/admin/layout.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import colors from "@/con/colors";
|
||||
@@ -30,7 +426,6 @@ import _ from "lodash";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
// import { useSnapshot } from "valtio";
|
||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
@@ -40,42 +435,53 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
const router = useRouter();
|
||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
|
||||
// const { user } = useSnapshot(authStore);
|
||||
|
||||
// console.log("Current user in store:", user);
|
||||
|
||||
// ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
const res = await fetch('/api/auth/me', {
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.user) {
|
||||
// Check if user is active
|
||||
// ✅ Check if user is NOT active → redirect to waiting room
|
||||
if (!data.user.isActive) {
|
||||
authStore.setUser(null);
|
||||
router.replace('/waiting-room');
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ PENTING: Selalu fetch menuIds terbaru setiap login
|
||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||
// ✅ Fetch menuIds
|
||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
});
|
||||
const menuData = await menuRes.json();
|
||||
|
||||
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||
? [...menuData.menuIds]
|
||||
: null;
|
||||
|
||||
// ✅ Set user dengan menuIds yang fresh dari database
|
||||
// ✅ Set user dengan menuIds yang fresh
|
||||
authStore.setUser({
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
roleId: Number(data.user.roleId),
|
||||
menuIds, // menuIds terbaru
|
||||
menuIds,
|
||||
isActive: data.user.isActive
|
||||
});
|
||||
|
||||
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (currentPath === '/admin') {
|
||||
const expectedPath = getRedirectPath(Number(data.user.roleId));
|
||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||
router.replace(expectedPath);
|
||||
}
|
||||
// ✅ Jangan redirect jika user sudah di path yang valid
|
||||
|
||||
} else {
|
||||
authStore.setUser(null);
|
||||
router.replace('/login');
|
||||
@@ -90,7 +496,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [router]); // ✅ Hapus dependency pada authStore.user
|
||||
}, [router]); // ✅ Only depend on router
|
||||
|
||||
const getRedirectPath = (roleId: number): string => {
|
||||
switch (roleId) {
|
||||
case 0: // DEVELOPER
|
||||
case 1: // SUPERADMIN
|
||||
case 2: // ADMIN_DESA
|
||||
return '/admin/landing-page/profil/program-inovasi';
|
||||
case 3: // ADMIN_KESEHATAN
|
||||
return '/admin/kesehatan/posyandu';
|
||||
case 4: // ADMIN_PENDIDIKAN
|
||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
default:
|
||||
return '/admin';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -104,7 +525,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Ambil menu berdasarkan roleId dan menuIds
|
||||
const currentNav = authStore.user
|
||||
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||
: [];
|
||||
@@ -112,30 +532,26 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setIsLoggingOut(true);
|
||||
|
||||
// ✅ Panggil API logout untuk clear session di server
|
||||
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
// Clear user data dari store
|
||||
authStore.setUser(null);
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('auth_nomor');
|
||||
localStorage.removeItem('auth_kodeId');
|
||||
|
||||
// Force reload untuk reset semua state
|
||||
localStorage.removeItem('auth_username');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
console.error('Logout failed:', result.message);
|
||||
// Tetap redirect meskipun gagal
|
||||
authStore.setUser(null);
|
||||
window.location.href = '/login';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
// Tetap clear store dan redirect jika error
|
||||
authStore.setUser(null);
|
||||
window.location.href = '/login';
|
||||
} finally {
|
||||
@@ -157,6 +573,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
||||
<AppShellHeader
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||
@@ -175,16 +592,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
h={{ base: 32, sm: 40 }}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
style={{
|
||||
minWidth: '32px',
|
||||
height: 'auto',
|
||||
}}
|
||||
style={{ minWidth: '32px', height: 'auto' }}
|
||||
/>
|
||||
<Text
|
||||
fw={700}
|
||||
c={colors["blue-button"]}
|
||||
fz={{ base: 'md', sm: 'xl' }}
|
||||
>
|
||||
<Text fw={700} c={colors["blue-button"]} fz={{ base: 'md', sm: 'xl' }}>
|
||||
Admin Darmasaba
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -192,63 +602,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<Group gap="xs">
|
||||
{!desktopOpened && (
|
||||
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
onClick={toggleDesktop}
|
||||
color={colors["blue-button"]}
|
||||
>
|
||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
||||
<IconChevronRight />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="md"
|
||||
color={colors["blue-button"]}
|
||||
mr="xs"
|
||||
/>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="md" color={colors["blue-button"]} mr="xs" />
|
||||
|
||||
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
router.push("/darmasaba");
|
||||
}}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
variant="gradient"
|
||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/darmasaba-icon.png"
|
||||
alt="Logo Darmasaba"
|
||||
w={20}
|
||||
h={20}
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
style={{
|
||||
minWidth: '20px',
|
||||
height: 'auto',
|
||||
}}
|
||||
/>
|
||||
<ActionIcon onClick={() => router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}>
|
||||
<Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={20} h={20} radius="md" loading="lazy" style={{ minWidth: '20px', height: 'auto' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={handleLogout}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
variant="gradient"
|
||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
loading={isLoggingOut}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<ActionIcon onClick={handleLogout} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }} loading={isLoggingOut} disabled={isLoggingOut}>
|
||||
<IconLogout2 size={22} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
@@ -256,75 +625,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Group>
|
||||
</AppShellHeader>
|
||||
|
||||
<AppShellNavbar
|
||||
component={ScrollArea}
|
||||
style={{
|
||||
background: "#ffffff",
|
||||
borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||
}}
|
||||
p={{ base: 'xs', sm: 'sm' }}
|
||||
>
|
||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
||||
{/* ... Navbar content sama seperti sebelumnya ... */}
|
||||
<AppShell.Section p="sm">
|
||||
{currentNav.map((v, k) => {
|
||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={k}
|
||||
defaultOpened={isParentActive}
|
||||
c={isParentActive ? colors["blue-button"] : "gray"}
|
||||
label={
|
||||
<Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||
{v.name}
|
||||
</Text>
|
||||
}
|
||||
style={{
|
||||
borderRadius: rem(10),
|
||||
marginBottom: rem(4),
|
||||
transition: "background 150ms ease",
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
variant="light"
|
||||
active={isParentActive}
|
||||
>
|
||||
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
||||
{v.children.map((child, key) => {
|
||||
const isChildActive = segments.includes(
|
||||
_.lowerCase(child.name)
|
||||
);
|
||||
|
||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||
return (
|
||||
<NavLink
|
||||
key={key}
|
||||
href={child.path}
|
||||
c={isChildActive ? colors["blue-button"] : "gray"}
|
||||
label={
|
||||
<Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||
{child.name}
|
||||
</Text>
|
||||
}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: rem(8),
|
||||
marginBottom: rem(2),
|
||||
transition: 'background 150ms ease',
|
||||
padding: '6px 12px',
|
||||
'&:hover': {
|
||||
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||
},
|
||||
...(isChildActive && {
|
||||
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
active={isChildActive}
|
||||
component={Link}
|
||||
/>
|
||||
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
||||
);
|
||||
})}
|
||||
</NavLink>
|
||||
@@ -334,18 +645,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
<AppShell.Section py="md">
|
||||
<Group justify="end" pr="sm">
|
||||
<Tooltip
|
||||
label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||
position="top"
|
||||
withArrow
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
onClick={toggleDesktop}
|
||||
color={colors["blue-button"]}
|
||||
>
|
||||
<Tooltip label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"} position="top" withArrow>
|
||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
||||
<IconChevronLeft />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
@@ -353,12 +654,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</AppShell.Section>
|
||||
</AppShellNavbar>
|
||||
|
||||
<AppShellMain
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<AppShellMain style={{ background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)", minHeight: "100vh" }}>
|
||||
{children}
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { getMenuIdsByRoleId } from "@/app/admin/(dashboard)/user&role/_com/getMenuIdByRole";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
@@ -23,7 +24,7 @@ export default async function userUpdate(context: Context) {
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { roleId: true, isActive: true }
|
||||
select: { roleId: true, isActive: true },
|
||||
});
|
||||
|
||||
if (!currentUser) {
|
||||
@@ -31,7 +32,29 @@ export default async function userUpdate(context: Context) {
|
||||
}
|
||||
|
||||
const isRoleChanged = roleId && currentUser.roleId !== roleId;
|
||||
const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive;
|
||||
const isActiveChanged =
|
||||
isActive !== undefined && currentUser.isActive !== isActive;
|
||||
|
||||
// ✅ Jika role berubah, reset dan set ulang akses menu
|
||||
if (isRoleChanged && roleId) {
|
||||
// Hapus akses lama
|
||||
await prisma.userMenuAccess.deleteMany({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
// Ambil menu default untuk role baru
|
||||
const menuIds = getMenuIdsByRoleId(roleId);
|
||||
|
||||
if (menuIds.length > 0) {
|
||||
// Buat akses baru
|
||||
await prisma.userMenuAccess.createMany({
|
||||
data: menuIds.map(menuId => ({
|
||||
userId: id,
|
||||
menuId
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update user
|
||||
const updatedUser = await prisma.user.update({
|
||||
@@ -48,10 +71,11 @@ export default async function userUpdate(context: Context) {
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
roleId: true,
|
||||
role: { select: { name: true } }
|
||||
}
|
||||
role: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// ✅ HAPUS SEMUA SESI USER DI DATABASE
|
||||
if (isRoleChanged) {
|
||||
await prisma.userSession.deleteMany({ where: { userId: id } });
|
||||
@@ -62,11 +86,13 @@ export default async function userUpdate(context: Context) {
|
||||
roleChanged: isRoleChanged,
|
||||
isActiveChanged,
|
||||
data: updatedUser,
|
||||
message: isRoleChanged
|
||||
message: isRoleChanged
|
||||
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
|
||||
: isActiveChanged
|
||||
? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.`
|
||||
: "User berhasil diupdate"
|
||||
? `${updatedUser.username} ${
|
||||
isActive ? "diaktifkan" : "dinonaktifkan"
|
||||
}.`
|
||||
: "User berhasil diupdate",
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error("❌ Error update user:", e);
|
||||
@@ -75,4 +101,4 @@ export default async function userUpdate(context: Context) {
|
||||
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,12 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||
// Verify OTP
|
||||
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||
where: { id: kodeId },
|
||||
select: { nomor: true, isActive: true }
|
||||
});
|
||||
|
||||
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "OTP tidak valid" },
|
||||
@@ -38,6 +43,7 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Check duplicate username
|
||||
if (await prisma.user.findFirst({ where: { username } })) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Username sudah digunakan" },
|
||||
@@ -45,12 +51,20 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 Tentukan roleId sebagai STRING
|
||||
const targetRoleId = "1"; // ✅ string, bukan number
|
||||
// Check duplicate nomor
|
||||
if (await prisma.user.findUnique({ where: { nomor: cleanNomor } })) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Nomor sudah terdaftar" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validasi role (gunakan string)
|
||||
// 🔥 Tentukan roleId sebagai STRING
|
||||
const targetRoleId = "2"; // ✅ Default ADMIN_DESA (roleId "2")
|
||||
|
||||
// Validasi role exists
|
||||
const roleExists = await prisma.role.findUnique({
|
||||
where: { id: targetRoleId }, // ✅ id bertipe string
|
||||
where: { id: targetRoleId },
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
@@ -61,17 +75,17 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Buat user dengan roleId string
|
||||
// ✅ Create user (inactive, waiting approval)
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
nomor,
|
||||
roleId: targetRoleId, // ✅ string
|
||||
isActive: false,
|
||||
nomor: cleanNomor,
|
||||
roleId: targetRoleId,
|
||||
isActive: false, // Waiting for admin approval
|
||||
},
|
||||
});
|
||||
|
||||
// Berikan akses menu
|
||||
// ✅ Berikan akses menu default based on role
|
||||
const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || [];
|
||||
if (menuIds.length > 0) {
|
||||
await prisma.userMenuAccess.createMany({
|
||||
@@ -79,14 +93,17 @@ export async function POST(req: Request) {
|
||||
userId: newUser.id,
|
||||
menuId,
|
||||
})),
|
||||
skipDuplicates: true, // ✅ Avoid duplicate errors
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Mark OTP as used
|
||||
await prisma.kodeOtp.update({
|
||||
where: { id: kodeId },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
// ✅ Create session token
|
||||
const token = await sessionCreate({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
@@ -95,25 +112,35 @@ export async function POST(req: Request) {
|
||||
id: newUser.id,
|
||||
nomor: newUser.nomor,
|
||||
username: newUser.username,
|
||||
roleId: newUser.roleId, // string
|
||||
isActive: false,
|
||||
roleId: newUser.roleId,
|
||||
isActive: false, // User belum aktif
|
||||
},
|
||||
invalidatePrevious: false,
|
||||
});
|
||||
|
||||
const response = NextResponse.redirect(new URL('/waiting-room', req.url));
|
||||
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||
// ✅ PENTING: Return JSON response (bukan redirect)
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
message: "Registrasi berhasil. Menunggu persetujuan admin.",
|
||||
userId: newUser.id,
|
||||
});
|
||||
|
||||
// ✅ Set session cookie
|
||||
const cookieName = process.env.BASE_SESSION_KEY || 'session';
|
||||
response.cookies.set(cookieName, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: 'lax',
|
||||
path: "/",
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Finalize Registration Error:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Registrasi gagal" },
|
||||
{ success: false, message: "Registrasi gagal. Silakan coba lagi." },
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET() {
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const [dbUser, menuAccess] = await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
|
||||
@@ -1,31 +1,105 @@
|
||||
/* styles/globals.css */
|
||||
|
||||
/* ===================================
|
||||
1. IMPORT CSS LIBRARIES
|
||||
=================================== */
|
||||
@import "@mantine/carousel/styles.css";
|
||||
@import "@mantine/dropzone/styles.css";
|
||||
@import "@mantine/charts/styles.css";
|
||||
@import "@mantine/dates/styles.css";
|
||||
@import "@mantine/tiptap/styles.css";
|
||||
@import "animate.css";
|
||||
@import "react-simple-toasts/dist/style.css";
|
||||
@import "react-simple-toasts/dist/theme/dark.css";
|
||||
@import "primereact/resources/themes/lara-light-blue/theme.css";
|
||||
@import "primereact/resources/primereact.min.css";
|
||||
@import "primeicons/primeicons.css";
|
||||
|
||||
/* ===================================
|
||||
2. FONT FACE - OPTIMIZED
|
||||
=================================== */
|
||||
@font-face {
|
||||
font-family: 'San Francisco';
|
||||
src: url('/assets/fonts/font.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap; /* ✅ TAMBAHKAN INI - Penting untuk PageSpeed! */
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
3. RESET & BASE STYLES
|
||||
=================================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%; /* Prevent font scaling in landscape */
|
||||
-moz-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'San Francisco', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
Roboto, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
4. GLASS EFFECTS - OPTIMIZED
|
||||
=================================== */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
will-change: transform; /* ✅ Hardware acceleration */
|
||||
}
|
||||
|
||||
.glass2 {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
will-change: transform; /* ✅ Hardware acceleration */
|
||||
}
|
||||
|
||||
.glass3 {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
will-change: transform; /* ✅ Hardware acceleration */
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
5. PERFORMANCE OPTIMIZATION
|
||||
=================================== */
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,5 @@
|
||||
// Import styles of packages that you've installed.
|
||||
// All packages except `@mantine/hooks` require styles imports
|
||||
import "@mantine/carousel/styles.css";
|
||||
import "@mantine/core/styles.css";
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import "animate.css";
|
||||
import 'react-simple-toasts/dist/style.css';
|
||||
import 'react-simple-toasts/dist/theme/dark.css';
|
||||
import "./globals.css";
|
||||
import '@mantine/charts/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/tiptap/styles.css';
|
||||
import "primereact/resources/themes/lara-light-blue/theme.css";
|
||||
import "primereact/resources/primereact.min.css";
|
||||
import "primeicons/primeicons.css";
|
||||
|
||||
|
||||
import "./globals.css"; // Sisanya import di globals.css
|
||||
|
||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||
import {
|
||||
@@ -23,19 +8,82 @@ import {
|
||||
createTheme,
|
||||
mantineHtmlProps,
|
||||
} from "@mantine/core";
|
||||
import { Metadata } from "next";
|
||||
import { ViewTransitions } from "next-view-transitions";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
export const metadata = {
|
||||
title: "Desa Darmasaba",
|
||||
description: "Desa Darmasaba Kabupaten Badung",
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Desa Darmasaba",
|
||||
template: "%s | Desa Darmasaba",
|
||||
},
|
||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
||||
keywords: [
|
||||
"desa darmasaba",
|
||||
"darmasaba",
|
||||
"badung",
|
||||
"bali",
|
||||
"desa",
|
||||
"pemerintah desa",
|
||||
"layanan publik",
|
||||
"abang batan desa",
|
||||
],
|
||||
authors: [{ name: "Pemerintah Desa Darmasaba" }],
|
||||
creator: "Desa Darmasaba",
|
||||
publisher: "Desa Darmasaba",
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
icon: "/assets/images/darmasaba-icon.png",
|
||||
apple: "/assets/images/darmasaba-icon.png",
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "id_ID",
|
||||
url: "https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
||||
siteName: "Desa Darmasaba",
|
||||
title: "Desa Darmasaba - Kabupaten Badung, Bali",
|
||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
||||
images: [
|
||||
{
|
||||
url: "/assets/images/darmasaba-icon.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Desa Darmasaba",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Desa Darmasaba - Kabupaten Badung, Bali",
|
||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali",
|
||||
images: ["/assets/images/darmasaba-icon.png"],
|
||||
},
|
||||
verification: {
|
||||
// google: "your-google-verification-code", // Tambahkan jika sudah punya
|
||||
// yandex: "your-yandex-verification-code",
|
||||
// yahoo: "your-yahoo-verification-code",
|
||||
},
|
||||
category: "government",
|
||||
other: {
|
||||
"msapplication-TileColor": "#ffffff",
|
||||
"theme-color": "#ffffff",
|
||||
},
|
||||
};
|
||||
|
||||
const theme = createTheme({
|
||||
fontFamily:
|
||||
"San Francisco, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
||||
fontFamilyMonospace:
|
||||
"SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace",
|
||||
fontFamily: "San Francisco, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
|
||||
fontFamilyMonospace: "SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
headings: { fontFamily: "San Francisco, sans-serif" },
|
||||
});
|
||||
|
||||
@@ -46,26 +94,22 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<ViewTransitions>
|
||||
<html lang="en" {...mantineHtmlProps}>
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<ColorSchemeScript />
|
||||
<link
|
||||
rel="icon"
|
||||
href="/assets/images/darmasaba-icon.png"
|
||||
sizes="any"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider theme={theme}>
|
||||
{children}
|
||||
|
||||
<LoadDataFirstClient />
|
||||
</MantineProvider>
|
||||
<ToastContainer position="bottom-center" hideProgressBar style={{
|
||||
zIndex: 9999
|
||||
}} />
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
hideProgressBar
|
||||
style={{ zIndex: 9999 }}
|
||||
/>
|
||||
</body>
|
||||
<LoadDataFirstClient />
|
||||
</html>
|
||||
</ViewTransitions>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,9 @@ import { useEffect, useState } from 'react';
|
||||
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
||||
|
||||
async function fetchUser() {
|
||||
const res = await fetch('/api/auth/me');
|
||||
const res = await fetch('/api/auth/me', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||
@@ -32,6 +34,7 @@ export default function WaitingRoom() {
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
Reference in New Issue
Block a user