Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct

This commit is contained in:
2025-10-17 10:03:03 +08:00
parent 0b574406e2
commit 75bf0652b1
25 changed files with 1420 additions and 356 deletions

View File

@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
align="center"
p="xs"
onClick={() => {
router.push(item.href);
stateNav.mobileOpen = false;
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}}
style={{ cursor: "pointer" }}
>
<Text c="dark.9" fw={600} fz="md">
{item.name}

View File

@@ -2,15 +2,14 @@
import colors from "@/con/colors"
import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core"
import { useHover } from "@mantine/hooks"
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { IconSearch, IconUser } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = {
@@ -21,6 +20,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter()
const next = useRouter()
const pathname = usePathname();
return (
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
@@ -47,7 +47,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip>
{listNavbar.map((item, k) => (
<MenuItemCom key={k} item={item} />
<MenuItemCom
key={k}
item={item}
isActive={pathname === item.href ||
(item.children?.some(child => child.href === pathname))}
/>
))}
<Tooltip label="Search content" position="bottom" withArrow>
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
)
}
function MenuItemCom({ item }: { item: MenuItem }) {
const { ref, hovered } = useHover()
function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
const router = useTransitionRouter()
return (
<Button
ref={ref}
color={hovered ? "gray" : colors["blue-button"]}
onMouseEnter={() => {
stateNav.item = item.children || null
stateNav.isSearch = false
<Menu
trigger="hover"
position="bottom-start"
offset={20}
width={300}
shadow="md"
withinPortal
onOpen={() => {
stateNav.item = item.children || null;
stateNav.isSearch = false;
}}
variant="subtle"
radius="xl"
onClick={() => {
router.push(item.href)
stateNav.clear()
}}
fw={500}
>
{item.name}
</Button>
<MenuTarget>
<Button
variant="subtle"
color={isActive ? 'blue' : 'gray'}
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.clear();
}
}}
styles={{
root: {
fontWeight: isActive ? 600 : 400,
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
'&:hover': {
backgroundColor: 'transparent',
}
}
}}
>
{item.name}
</Button>
</MenuTarget>
</Menu>
)
}

View File

@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
import { MenuItem } from "../../../../types/menu-item";
import { useTransitionRouter } from "next-view-transitions";
import colors from "@/con/colors";
import { usePathname } from "next/navigation";
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter();
const pathname = usePathname();
return (
<motion.div
key={Math.random().toString(36).slice(2)}
@@ -32,33 +33,34 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
<Stack gap="xs" align="stretch">
{item.map((link, index) => (
<Button
key={index}
variant="subtle"
justify="space-between"
size="lg"
radius="md"
color="gray.0"
onClick={() => {
key={index}
variant="subtle"
justify="space-between"
size="lg"
radius="md"
color={pathname === link.href ? 'blue' : 'gray'}
onClick={() => {
if (link.href) {
router.push(link.href);
stateNav.item = null;
stateNav.isSearch = false;
}}
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: "transparent",
color: colors['blue-button'],
fontWeight: 500,
transition: "all 0.2s ease",
"&:hover": {
background: theme.colors.gray[8],
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
},
},
})}
>
{link.name}
</Button>
}
}}
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: pathname === link.href ? 600 : 500,
transition: "all 0.2s ease",
"&:hover": {
background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
}
},
})}
>
{link.name}
</Button>
))}
</Stack>
) : (

View File

@@ -1,21 +1,24 @@
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Stack, Text, TextInput } from '@mantine/core';
import { Box, Center, Loader, Modal, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [isOpen, setIsOpen] = useState(false);
// Toggle modal when there's a query
useEffect(() => {
setIsOpen(!!snap.query);
}, [snap.query]);
// Infinite scroll
useEffect(() => {
const handleScroll = () => {
const bottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
@@ -23,15 +26,14 @@ export default function GlobalSearch() {
}, [snap.loading]);
return (
<Stack maw={800} mx="auto">
{/* 🔍 Search input */}
<Box style={{ position: 'relative', width: '100%' }}>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => (
searchState.query = e.currentTarget.value,
debouncedFetch()
)}
onChange={(e) => {
searchState.query = e.currentTarget.value;
debouncedFetch();
}}
radius="xl"
rightSection={
snap.query ? (
@@ -47,45 +49,64 @@ export default function GlobalSearch() {
}
/>
{/* 📄 Hasil pencarian */}
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{snap.results.map((item, i) => (
<Box
key={i}
p="sm"
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
{/* Modal for search results */}
<Modal
opened={isOpen && !!snap.query}
onClose={() => {
searchState.query = '';
searchState.results = [];
}}
withCloseButton={false}
size="lg"
padding={0}
radius="md"
style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1000 }}
styles={{
content: { // Changed from 'modal' to 'content'
backgroundColor: 'white',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
borderRadius: '0.5rem',
maxHeight: '400px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%'
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={() => {
const url = getDetailUrl(item);
window.location.href = url;
}}
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
</Text>
},
}}
>
<Box style={{ maxHeight: '400px', overflowY: 'auto' }}>
{snap.results.map((item, i) => (
<Box
key={i}
p="sm"
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={() => {
const url = getDetailUrl(item);
window.location.href = url;
}}
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
</Text>
</Box>
))}
{snap.loading && (
<Center py="md">
<Loader size="sm" />
</Center>
)}
</Box>
))}
</div>
{/* ⏳ Loader di bawah hasil */}
{snap.loading && (
<Center py="md">
<Loader size="sm" />
</Center>
)}
</Stack>
</Modal>
</Box>
);
}
}

View File

@@ -332,7 +332,7 @@ function Kepuasan() {
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;

View File

@@ -6,20 +6,19 @@ import {
Center,
Image,
Paper,
ScrollArea,
SimpleGrid,
Skeleton,
Stack,
Text,
Tooltip,
Skeleton,
useMantineColorScheme,
ScrollArea,
useMantineColorScheme
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { useTransitionRouter } from "next-view-transitions";
import { useProxy } from "valtio/utils";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
@@ -30,7 +29,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return (
<motion.div whileHover={{ scale: 1.03 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg"
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
</Text>
</Box>
</Paper>
</Tooltip>
</motion.div>
);
}

View File

@@ -37,9 +37,13 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="cover"
fit="contain"
radius="lg"
loading="lazy"
style={{
objectPosition: 'bottom center',
transform: 'translateY(10px)', // sedikit turun biar natural
}}
/>
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
@@ -49,13 +53,26 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Text>
</Stack>
)}
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
>
<Card
px="lg"
radius="2xl"
py="sm"
radius="lg"
withBorder
className="glass3"
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
}}
>
<Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed">

View File

@@ -51,7 +51,7 @@ export default function SDGS() {
</Title>
</Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
</Text>
<Box py={50}>
@@ -67,49 +67,62 @@ export default function SDGS() {
>
{sdgsDesa && sdgsDesa.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
{sdgsDesa.map((item) => (
<Paper
key={item.id}
p="lg"
radius="xl"
shadow="sm"
withBorder
style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
}}
>
<Center mb="lg">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<Image
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
alt={item.name}
w={mobile ? 90 : 110}
h={mobile ? 90 : 110}
fit="contain"
loading="lazy"
/>
</Box>
</Center>
{sdgsDesa.map((item) => (
<Paper
key={item.id}
p="lg"
radius="xl"
shadow="sm"
withBorder
style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
display: "flex",
flexDirection: "column",
}}
>
<Center mb="lg">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<Image
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
alt={item.name}
w={mobile ? 90 : 110}
h={mobile ? 90 : 110}
fit="contain"
loading="lazy"
/>
</Box>
</Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
<Text ta="center" fz={{ base: "lg", md: "xl" }} fw={700} mb="xs">
<Text
ta="center"
fz={{ base: "lg", md: "xl" }}
fw={700}
mb="xs"
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
>
{item.name}
</Text>
</Tooltip>
<Title
order={2}
ta="center"
@@ -122,9 +135,11 @@ export default function SDGS() {
>
{item.jumlah}
</Title>
</Paper>
))}
</SimpleGrid>
</Stack>
</Paper>
))}
</SimpleGrid>
) : (
<Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />

View File

@@ -52,6 +52,37 @@ const getDetailUrl = (item: { type?: string; id: string | number; [key: string]:
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: '/darmasaba/lingkungan/gotong-royong',
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/tujuan-edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/materi-edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/contoh-edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/filosofi-tri-hita',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/bentuk-konservasi-berdasarkan-adat',
nilaiKonservasiAdat: '/darmasaba/lingkungan/nilai-konservasi-adat',
jenjangPendidikan: '/darmasaba/inovasi/jenjang-pendidikan',
lembaga: '/darmasaba/inovasi/lembaga',
siswa: '/darmasaba/inovasi/siswa',
pengajar: '/darmasaba/inovasi/pengajar',
keunggulanProgram: '/darmasaba/inovasi/keunggulan-program',
tujuanProgram: '/darmasaba/inovasi/tujuan-program',
programUnggulan: '/darmasaba/inovasi/program-unggulan',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/inovasi/lokasi-jadwal-bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/inovasi/fasilitas-bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/inovasi/tujuan-pendidikan-non-formal',
tempatKegiatan: '/darmasaba/inovasi/tempat-kegiatan',
jenisProgramYangDiselenggarakan: '/darmasaba/inovasi/jenis-program-yang-diselenggarakan',
dataPerpustakaan: '/darmasaba/inovasi/data-perpustakaan',
dataPendidikan: '/darmasaba/inovasi/data-pendidikan',
};
return typeUrlMap[type || ''] || '/darmasaba';