diff --git a/bun.lockb b/bun.lockb index 0da4afd1..6da03f4d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index eb085cc2..a737af3f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "framer-motion": "^12.23.5", "get-port": "^7.1.0", "jotai": "^2.12.3", + "list": "^2.0.19", "lodash": "^4.17.21", "motion": "^12.4.1", "nanoid": "^5.1.5", diff --git a/src/app/admin/(dashboard)/_state/desa/berita.ts b/src/app/admin/(dashboard)/_state/desa/berita.ts index b314fcd4..d49562b4 100644 --- a/src/app/admin/(dashboard)/_state/desa/berita.ts +++ b/src/app/admin/(dashboard)/_state/desa/berita.ts @@ -263,6 +263,59 @@ const berita = proxy({ berita.edit.form = { ...defaultForm }; }, }, + findFirst: { + data: null as Prisma.BeritaGetPayload<{ + include: { + image: true; + kategoriBerita: true; + }; + }> | null, + loading: false, + async load() { + this.loading = true; + try { + const res = await ApiFetch.api.desa.berita["find-first"].get(); + if (res.status === 200 && res.data?.success) { + // Add type assertion to ensure type safety + berita.findFirst.data = res.data.data as Prisma.BeritaGetPayload<{ + include: { + image: true; + kategoriBerita: true; + }; + }> | null; + } + } catch (err) { + console.error("Gagal fetch berita terbaru:", err); + } finally { + this.loading = false; + } + }, + }, + findRecent: { + data: [] as Prisma.BeritaGetPayload<{ + include: { + image: true; + kategoriBerita: true; + }; + }>[], + loading: false, + + async load() { + try { + this.loading = true; + const res = await ApiFetch.api.desa.berita["find-recent"].get(); + if (res.status === 200 && res.data?.success) { + this.data = res.data.data ?? []; + } + } catch (error) { + console.error("Gagal fetch berita recent:", error); + } finally { + this.loading = false; + } + }, + } + + }); diff --git a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts index 91b4e311..3c9ae6ae 100644 --- a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts +++ b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts @@ -68,11 +68,11 @@ const pengumuman = proxy({ }, findMany: { data: null as - | Prisma.PengumumanGetPayload<{ - include: { + | Prisma.PengumumanGetPayload<{ + include: { CategoryPengumuman: true; - } - }>[] + }; + }>[] | null, async load() { const res = await ApiFetch.api.desa.pengumuman["find-many"].get(); @@ -82,30 +82,28 @@ const pengumuman = proxy({ } }, }, - // findUnique: { - // data: null as - // | Prisma.PengumumanGetPayload<{ - // include: { - // CategoryPengumuman: true; - // } - // }> - // | null, - // async load(id: string) { - // try { - // const res = await fetch(`/api/desa/pengumuman/${id}`); - // if (res.ok) { - // const data = await res.json(); - // pengumuman.findUnique.data = data.data ?? null; - // } else { - // console.error('Failed to fetch pengumuman:', res.statusText); - // pengumuman.findUnique.data = null; - // } - // } catch (error) { - // console.error('Error fetching pengumuman:', error); - // pengumuman.findUnique.data = null; - // } - // }, - // }, + findUnique: { + data: null as Prisma.PengumumanGetPayload<{ + include: { + CategoryPengumuman: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/desa/pengumuman/${id}`); + if (res.ok) { + const data = await res.json(); + pengumuman.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch pengumuman:", res.statusText); + pengumuman.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching pengumuman:", error); + pengumuman.findUnique.data = null; + } + }, + }, delete: { loading: false, async byId(id: string) { @@ -237,6 +235,55 @@ const pengumuman = proxy({ } }, }, + findFirst: { + data: null as Prisma.PengumumanGetPayload<{ + include: { + CategoryPengumuman: true; + }; + }> | null, + loading: false, + async load() { + this.loading = true; + try { + const res = await ApiFetch.api.desa.pengumuman["find-first"].get(); + if (res.status === 200 && res.data?.success) { + // Add type assertion to ensure type safety + pengumuman.findFirst.data = res.data + .data as Prisma.PengumumanGetPayload<{ + include: { + CategoryPengumuman: true; + }; + }> | null; + } + } catch (err) { + console.error("Gagal fetch pengumuman terbaru:", err); + } finally { + this.loading = false; + } + }, + }, + findRecent: { + data: [] as Prisma.PengumumanGetPayload<{ + include: { + CategoryPengumuman: true; + }; + }>[], + loading: false, + + async load() { + try { + this.loading = true; + const res = await ApiFetch.api.desa.pengumuman["find-recent"].get(); + if (res.status === 200 && res.data?.success) { + this.data = res.data.data ?? []; + } + } catch (error) { + console.error("Gagal fetch pengumuman recent:", error); + } finally { + this.loading = false; + } + }, + }, }); const stateDesaPengumuman = proxy({ diff --git a/src/app/api/[[...slugs]]/_lib/desa/berita/findFirst.ts b/src/app/api/[[...slugs]]/_lib/desa/berita/findFirst.ts new file mode 100644 index 00000000..383a4cec --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/berita/findFirst.ts @@ -0,0 +1,30 @@ +import prisma from '@/lib/prisma'; + +export default async function beritaFindFirst() { + try { + const result = await prisma.berita.findFirst({ + where: { + isActive: true, // opsional kalau kamu punya field ini + }, + orderBy: { + createdAt: 'desc', // ambil yang paling terbaru + }, + include: { + image: true, + kategoriBerita: true, + } + }); + + return { + success: true, + message: 'Berhasil ambil berita terbaru', + data: result, + }; + } catch (error) { + console.error('[findFirstBerita] Error:', error); + return { + success: false, + message: 'Gagal ambil berita terbaru', + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/berita/findRecent.ts b/src/app/api/[[...slugs]]/_lib/desa/berita/findRecent.ts new file mode 100644 index 00000000..5e79b4cc --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/berita/findRecent.ts @@ -0,0 +1,19 @@ +import prisma from "@/lib/prisma"; + +export default async function findRecentBerita() { + const result = await prisma.berita.findMany({ + orderBy: { + createdAt: "desc", + }, + take: 4, // ambil 4 data terbaru + include: { + image: true, + kategoriBerita: true, + }, + }); + + return { + success: true, + data: result, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/berita/index.ts b/src/app/api/[[...slugs]]/_lib/desa/berita/index.ts index 3fdf3b63..3959faf0 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/berita/index.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/berita/index.ts @@ -5,6 +5,8 @@ import beritaCreate from "./create"; import beritaDelete from "./del"; import beritaUpdate from "./updt"; import findBeritaById from "./find-by-id"; +import beritaFindFirst from "./findFirst"; +import findRecentBerita from "./findRecent"; const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] }) .get("/category/find-many", kategoriBeritaFindMany) @@ -22,6 +24,8 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] }) kategoriBeritaId: t.Union([t.String(), t.Null()]), }), }) + .get("/find-first", beritaFindFirst) + .get("/find-recent", findRecentBerita) .delete("/delete/:id", beritaDelete) .put( "/:id", @@ -39,5 +43,6 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] }) }), } ); + export default Berita; diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findFirst.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findFirst.ts new file mode 100644 index 00000000..94ef45a3 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findFirst.ts @@ -0,0 +1,29 @@ +import prisma from '@/lib/prisma'; + +export default async function pengumumanFindFirst() { + try { + const result = await prisma.pengumuman.findFirst({ + where: { + isActive: true, // opsional kalau kamu punya field ini + }, + orderBy: { + createdAt: 'desc', // ambil yang paling terbaru + }, + include: { + CategoryPengumuman: true, + } + }); + + return { + success: true, + message: 'Berhasil ambil pengumuman terbaru', + data: result, + }; + } catch (error) { + console.error('[findFirstPengumuman] Error:', error); + return { + success: false, + message: 'Gagal ambil pengumuman terbaru', + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findRecent.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findRecent.ts new file mode 100644 index 00000000..a37fbf84 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/findRecent.ts @@ -0,0 +1,18 @@ +import prisma from "@/lib/prisma"; + +export default async function pengumumanFindRecent() { + const result = await prisma.pengumuman.findMany({ + orderBy: { + createdAt: "desc", + }, + take: 4, // ambil 4 data terbaru + include: { + CategoryPengumuman: true, + }, + }); + + return { + success: true, + data: result, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts index 0b059ea5..3bcb683e 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts @@ -6,6 +6,8 @@ import pengumumanCategoryFindMany from "./category"; import pengumumanDelete from "./del"; import pengumumanFindById from "./find-by-id"; import pengumumanUpdate from "./updt"; +import pengumumanFindFirst from "./findFirst"; +import pengumumanFindRecent from "./findRecent"; const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] }) .get("/category/find-many", pengumumanCategoryFindMany) @@ -20,6 +22,8 @@ const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] categoryPengumumanId: t.Union([t.String(), t.Null()]), }), }) + .get("/find-first", pengumumanFindFirst) + .get("/find-recent", pengumumanFindRecent) .put("/:id", pengumumanUpdate, { body: t.Object({ id: t.String(), diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/page.tsx index 9df154bb..4ac2d9c8 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/page.tsx @@ -1,27 +1,136 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' +import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import colors from '@/con/colors'; -import { Box, Paper, Stack, Text } from '@mantine/core'; -import { IconBell } from '@tabler/icons-react'; -import { motion } from 'framer-motion'; +import { Box, Card, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import { useEffect } from 'react'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../../desa/layanan/_com/BackButto'; +import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; + +dayjs.extend(relativeTime); function InformasiDesa() { + const stateBerita = useProxy(stateDashboardBerita.berita) + const statePengumuman = useProxy(stateDesaPengumuman.pengumuman) + + useEffect(() => { + stateBerita.findFirst.load(); + stateBerita.findRecent.load(); + statePengumuman.findFirst.load(); + statePengumuman.findRecent.load(); + }, []); + + const dataBerita = stateBerita.findFirst.data + const dataPengumuman = statePengumuman.findFirst.data + + return ( - - - - - - , - - Informasi Desa - Akses berita dan pengumuman terbaru seputar kegiatan desa - - + + + + + + + Informasi Desa + + + + + {dataBerita && ( + + + + {dataBerita.judul} + + + + {dataBerita.kategoriBerita?.name} • {dayjs(dataBerita.createdAt).fromNow()} + {dataBerita.judul} + + + + + + )} + + Berita Terbaru + + {stateBerita.findRecent.data.map((item) => ( + + + + {item.judul} + + + + {item.judul} + + + {item.deskripsi} + + + {dayjs(item.createdAt).fromNow()} + + + + + ))} + + + + + {dataPengumuman && ( + + + {dataPengumuman.judul} + {dataPengumuman.CategoryPengumuman?.name} • {dayjs(dataPengumuman.createdAt).fromNow()} + + + + + + )} + + Pengumuman Terbaru + + {statePengumuman.findRecent.data.map((item) => ( + + + + + {item.judul} + + + {item.deskripsi} + + + {dayjs(item.createdAt).fromNow()} + + + + + ))} + + + - + + ); } diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/page.tsx index bd8ddc34..13145970 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/page.tsx @@ -1,13 +1,16 @@ 'use client' import colors from '@/con/colors'; -import { Box, SimpleGrid, Stack, Text } from '@mantine/core'; +import { Box, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; +import { IconBell } from '@tabler/icons-react'; +import { motion } from 'framer-motion'; +import { useRouter } from 'next/navigation'; import BackButton from '../../desa/layanan/_com/BackButto'; import AdministrasiOnline from './administrasi-online/page'; -import InformasiDesa from './informasi-desa/page'; import PengaduanMasyarakat from './pengaduan-masyarakat/page'; function Page() { + const router = useRouter() return ( @@ -29,7 +32,23 @@ function Page() { - + + + router.push('/darmasaba/inovasi/layanan-online-desa/informasi-desa')} + > + + + + + Informasi Desa + Akses berita dan pengumuman terbaru seputar kegiatan desa + + + + diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index ce78eb08..e1641ea9 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -82,7 +82,7 @@ function PengaduanMasyarakat() { > - , + Pengaduan Masyarakat Sampaikan keluhan dan aspirasi Anda melalui platform digital kami diff --git a/src/app/darmasaba/_com/Navbar.tsx b/src/app/darmasaba/_com/Navbar.tsx index 1c1fb433..404193ab 100644 --- a/src/app/darmasaba/_com/Navbar.tsx +++ b/src/app/darmasaba/_com/Navbar.tsx @@ -34,7 +34,7 @@ export function Navbar() { }} size={80} radius={"xl"} > - Logo Desa + Logo Desa stateNav.mobileOpen = !stateNav.mobileOpen} color={colors["blue-button"]} opened={mobileOpen} />