API & UI Menu Inovasi, SubMenu Layanan Online Desa
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
30
src/app/api/[[...slugs]]/_lib/desa/berita/findFirst.ts
Normal file
30
src/app/api/[[...slugs]]/_lib/desa/berita/findFirst.ts
Normal file
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
19
src/app/api/[[...slugs]]/_lib/desa/berita/findRecent.ts
Normal file
19
src/app/api/[[...slugs]]/_lib/desa/berita/findRecent.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
29
src/app/api/[[...slugs]]/_lib/desa/pengumuman/findFirst.ts
Normal file
29
src/app/api/[[...slugs]]/_lib/desa/pengumuman/findFirst.ts
Normal file
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
18
src/app/api/[[...slugs]]/_lib/desa/pengumuman/findRecent.ts
Normal file
18
src/app/api/[[...slugs]]/_lib/desa/pengumuman/findRecent.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 (
|
||||
<Box>
|
||||
<Stack >
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.8 }}
|
||||
>
|
||||
<Paper p={'xl'} >
|
||||
<Box>
|
||||
<IconBell size={50} color={colors['blue-button']} />,
|
||||
</Box>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Informasi Desa</Text>
|
||||
<Text fz={'lg'} c={'black'}>Akses berita dan pengumuman terbaru seputar kegiatan desa</Text>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Informasi Desa
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={10}>
|
||||
{dataBerita && (
|
||||
<Paper shadow="md" radius="md" p="md">
|
||||
<Grid>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
<Image
|
||||
src={dataBerita.image?.link || "/fallback.jpg"}
|
||||
alt={dataBerita.judul}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
height={250}
|
||||
maw={600}
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
<Box>
|
||||
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} • {dayjs(dataBerita.createdAt).fromNow()}</Text>
|
||||
<Title order={1} fw="bold">{dataBerita.judul}</Title>
|
||||
<Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataBerita.content }} />
|
||||
</Box>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)}
|
||||
<Stack py={10}>
|
||||
<Title order={3}>Berita Terbaru</Title>
|
||||
<Grid>
|
||||
{stateBerita.findRecent.data.map((item) => (
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
|
||||
<Card shadow="sm" radius="md" withBorder h="100%">
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || "/placeholder.jpg"}
|
||||
alt={item.judul}
|
||||
height={160} // gambar fix height
|
||||
fit="cover"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Stack gap="xs" mt="sm">
|
||||
<Text fw={600} lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={2}>
|
||||
{item.deskripsi}
|
||||
</Text>
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
</Stack>
|
||||
<Divider color={colors['blue-button']} my="md" />
|
||||
{dataPengumuman && (
|
||||
<Paper shadow="md" radius="md" p="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={1} fw="bold">{dataPengumuman.judul}</Title>
|
||||
<Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} • {dayjs(dataPengumuman.createdAt).fromNow()}</Text>
|
||||
<Box>
|
||||
<Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
<Stack py={10}>
|
||||
<Title order={3}>Pengumuman Terbaru</Title>
|
||||
<Grid>
|
||||
{statePengumuman.findRecent.data.map((item) => (
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
|
||||
<Card shadow="sm" radius="md" withBorder h="100%">
|
||||
<Stack gap="xs" mt="sm">
|
||||
<Text fw={600} lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={2}>
|
||||
{item.deskripsi}
|
||||
</Text>
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -29,7 +32,23 @@ function Page() {
|
||||
<Stack gap={'lg'}>
|
||||
<AdministrasiOnline />
|
||||
<PengaduanMasyarakat />
|
||||
<InformasiDesa />
|
||||
<Box>
|
||||
<Stack>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.8 }}
|
||||
onClick={() => router.push('/darmasaba/inovasi/layanan-online-desa/informasi-desa')}
|
||||
>
|
||||
<Paper p={'xl'} >
|
||||
<Box>
|
||||
<IconBell size={50} color={colors['blue-button']} />
|
||||
</Box>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Informasi Desa</Text>
|
||||
<Text fz={'lg'} c={'black'}>Akses berita dan pengumuman terbaru seputar kegiatan desa</Text>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
@@ -82,7 +82,7 @@ function PengaduanMasyarakat() {
|
||||
>
|
||||
<Paper p={'xl'} >
|
||||
<Box>
|
||||
<IconMessageCircleQuestion size={50} color={colors['blue-button']} />,
|
||||
<IconMessageCircleQuestion size={50} color={colors['blue-button']} />
|
||||
</Box>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Pengaduan Masyarakat</Text>
|
||||
<Text fz={'lg'} c={'black'}>Sampaikan keluhan dan aspirasi Anda melalui platform digital kami</Text>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function Navbar() {
|
||||
}}
|
||||
size={80} radius={"xl"}
|
||||
>
|
||||
<Image src="/api/img/darmasaba-icon.png" alt="Logo Desa" width={50} height={50} />
|
||||
<Image src="/darmasaba-icon.png" alt="Logo Desa" width={50} height={50} />
|
||||
</ActionIcon>
|
||||
<Burger onClick={() => stateNav.mobileOpen = !stateNav.mobileOpen} color={colors["blue-button"]} opened={mobileOpen} />
|
||||
</Group>
|
||||
|
||||
Reference in New Issue
Block a user