API & UI Menu Lingkungan Submenu Pengelolaan Sampah

This commit is contained in:
2025-07-18 15:01:43 +08:00
parent 7439eb7687
commit 4025771a4d
25 changed files with 1235 additions and 304 deletions

View File

@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
import { useEffect, useState } from 'react';
import 'leaflet/dist/leaflet.css';
import L, { LeafletMouseEvent } from 'leaflet';
type Props = {
defaultCenter: { lat: number; lng: number };
onSelect?: (pos: { lat: number; lng: number }) => void;
readOnly?: boolean;
};
export default function LeafletMap({ defaultCenter, onSelect, readOnly = false }: Props) {
const [markerPos, setMarkerPos] = useState(defaultCenter);
useEffect(() => {
// Aman di sini, karena ini hanya jalan di client
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
}, []);
function LocationMarker() {
useMapEvents({
click(e: LeafletMouseEvent) {
if (readOnly) return;
const { lat, lng } = e.latlng;
setMarkerPos({ lat, lng });
onSelect?.({ lat, lng });
},
});
return <Marker position={markerPos} />;
}
return (
<MapContainer
center={defaultCenter}
zoom={16}
scrollWheelZoom
style={{ height: '100%', width: '100%', zIndex: 0 }}
>
<TileLayer
attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a>'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LocationMarker />
</MapContainer>
);
}

View File

@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
import { useState, useEffect } from 'react';
import 'leaflet/dist/leaflet.css';
import L, { LeafletMouseEvent } from 'leaflet';
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
type Props = {
initialPosition: { lat: number; lng: number };
onChange: (pos: { lat: number; lng: number }) => void;
};
export default function LeafletMapEdit({ initialPosition, onChange }: Props) {
const [markerPos, setMarkerPos] = useState(initialPosition);
useEffect(() => {
setMarkerPos(initialPosition);
}, [initialPosition]);
function LocationMarker() {
useMapEvents({
click(e: LeafletMouseEvent) {
const { lat, lng } = e.latlng;
setMarkerPos({ lat, lng });
onChange({ lat, lng });
},
});
return <Marker position={markerPos} />;
}
return (
<MapContainer
center={markerPos}
zoom={16}
scrollWheelZoom
style={{ height: '100%', width: '100%', zIndex: 0 }}
>
<TileLayer
attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a>'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LocationMarker />
</MapContainer>
);
}

View File

@@ -15,12 +15,12 @@ const defaultForm = {
icon: "",
};
const pengelolaanSampahState = proxy({
const pengelolaanSampah = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(pengelolaanSampahState.create.form);
const cek = templateForm.safeParse(pengelolaanSampah.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -29,12 +29,12 @@ const pengelolaanSampahState = proxy({
}
try {
pengelolaanSampahState.create.loading = true;
const res = await ApiFetch.api.lingkungan.pengelolaansampah["create"].post(
pengelolaanSampahState.create.form
);
pengelolaanSampah.create.loading = true;
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"create"
].post(pengelolaanSampah.create.form);
if (res.status === 200) {
pengelolaanSampahState.findMany.load();
pengelolaanSampah.findMany.load();
return toast.success("success create");
}
console.log(res);
@@ -42,7 +42,7 @@ const pengelolaanSampahState = proxy({
} catch (error) {
console.log((error as Error).message);
} finally {
pengelolaanSampahState.create.loading = false;
pengelolaanSampah.create.loading = false;
}
},
},
@@ -54,33 +54,35 @@ const pengelolaanSampahState = proxy({
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pengelolaanSampahState.findMany.loading = true; // Use the full path to access the property
pengelolaanSampahState.findMany.page = page;
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
pengelolaanSampah.findMany.page = page;
try {
const res = await ApiFetch.api.lingkungan.pengelolaansampah["find-many"].get({
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pengelolaanSampahState.findMany.data = res.data.data || [];
pengelolaanSampahState.findMany.total = res.data.total || 0;
pengelolaanSampahState.findMany.totalPages = res.data.totalPages || 1;
pengelolaanSampah.findMany.data = res.data.data || [];
pengelolaanSampah.findMany.total = res.data.total || 0;
pengelolaanSampah.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pengelolaan sampah:",
res.data?.message
);
pengelolaanSampahState.findMany.data = [];
pengelolaanSampahState.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1;
pengelolaanSampah.findMany.data = [];
pengelolaanSampah.findMany.total = 0;
pengelolaanSampah.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findMany.data = [];
pengelolaanSampahState.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1;
pengelolaanSampah.findMany.data = [];
pengelolaanSampah.findMany.total = 0;
pengelolaanSampah.findMany.totalPages = 1;
} finally {
pengelolaanSampahState.findMany.loading = false;
pengelolaanSampah.findMany.loading = false;
}
},
},
@@ -95,12 +97,15 @@ const pengelolaanSampahState = proxy({
}
try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/lingkungan/pengelolaansampah/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -143,19 +148,22 @@ const pengelolaanSampahState = proxy({
}
this.loading = true;
try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const response = await fetch(
`/api/lingkungan/pengelolaansampah/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await pengelolaanSampahState.findMany.load();
await pengelolaanSampah.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
@@ -174,14 +182,14 @@ const pengelolaanSampahState = proxy({
const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`);
if (res.ok) {
const data = await res.json();
pengelolaanSampahState.findUnique.data = data.data ?? null;
pengelolaanSampah.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pengelolaanSampahState.findUnique.data = null;
pengelolaanSampah.findUnique.data = null;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findUnique.data = null;
pengelolaanSampah.findUnique.data = null;
}
},
},
@@ -191,20 +199,25 @@ const pengelolaanSampahState = proxy({
if (!id) return toast.warn("ID tidak valid");
try {
pengelolaanSampahState.delete.loading = true;
pengelolaanSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/lingkungan/pengelolaansampah/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "pengelolaan sampah berhasil dihapus");
await pengelolaanSampahState.findMany.load(); // refresh list
toast.success(
result.message || "pengelolaan sampah berhasil dihapus"
);
await pengelolaanSampah.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengelolaan sampah");
}
@@ -212,10 +225,236 @@ const pengelolaanSampahState = proxy({
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah");
} finally {
pengelolaanSampahState.delete.loading = false;
pengelolaanSampah.delete.loading = false;
}
},
},
});
const templateKeteranganSampahForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
lat: z.number(),
lng: z.number(),
});
const defaultKeteranganSampahForm = {
name: "",
alamat: "",
namaTempatMaps: "",
lat: 0,
lng: 0,
};
const keteranganSampah = proxy({
create: {
form: { ...defaultKeteranganSampahForm },
loading: false,
async create() {
const cek = templateKeteranganSampahForm.safeParse(
keteranganSampah.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keteranganSampah.create.loading = true;
const res =
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
"create"
].post(keteranganSampah.create.form);
if (res.status === 200) {
keteranganSampah.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
keteranganSampah.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.KeteranganBankSampahTerdekatGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
"find-many"
].get();
if (res.status === 200) {
keteranganSampah.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
if (res.ok) {
const data = await res.json();
keteranganSampah.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
keteranganSampah.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
keteranganSampah.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
keteranganSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Keterangan sampah berhasil dihapus");
await keteranganSampah.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus keterangan sampah");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus keterangan sampah");
} finally {
keteranganSampah.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKeteranganSampahForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keterangan sampah:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKeteranganSampahForm.safeParse(keteranganSampah.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keteranganSampah.edit.loading = true;
const response = await fetch(
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
alamat: this.form.alamat,
namaTempatMaps: this.form.namaTempatMaps,
lat: this.form.lat,
lng: this.form.lng,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update keterangan sampah");
await keteranganSampah.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate keterangan sampah");
}
} catch (error) {
console.error("Error updating keterangan sampah:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate keterangan sampah"
);
return false;
} finally {
keteranganSampah.edit.loading = false;
}
},
reset() {
keteranganSampah.edit.id = "";
keteranganSampah.edit.form = { ...defaultKeteranganSampahForm };
},
},
});
const pengelolaanSampahState = proxy({
pengelolaanSampah,
keteranganSampah,
});
export default pengelolaanSampahState;

View File

@@ -28,13 +28,7 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan"
},
{
label: "Informasi Desa",
value: "informasidesa",
href: "/admin/inovasi/layanan-online-desa/informasi-desa"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);

View File

@@ -0,0 +1,141 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
const LeafletMapEdit = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapEdit'), { ssr: false });
function EditKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
const params = useParams()
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const [formData, setFormData] = useState({
name: '',
alamat: '',
namaTempatMaps: '',
lat: 0,
lng: 0,
})
useEffect(() => {
const loadKeteranganBankSampahTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keteranganState.edit.load(id);
if (data) {
keteranganState.edit.id = id;
keteranganState.edit.form = {
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
};
setFormData({
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
});
setMarkerPosition({ lat: data.lat, lng: data.lng });
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadKeteranganBankSampahTerdekat();
}, [params?.id]);
const handleSubmit = async () => {
try {
keteranganState.edit.form = {
...keteranganState.edit.form,
name: formData.name.trim(),
alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(),
lat: formData.lat,
lng: formData.lng,
}
await keteranganState.edit.update();
keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title>
<TextInput
value={formData.name}
onChange={(val) => setFormData({ ...formData, name: val.target.value })}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama Bank Sampah Terdekat'
/>
<TextInput
value={formData.alamat}
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })}
label={<Text fw="bold" fz="sm">Alamat</Text>}
placeholder='Masukkan alamat Bank Sampah'
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
placeholder='Masukkan nama tempat maps Bank Sampah'
/>
<Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
<Box style={{ height: 300, width: '100%' }}>
<LeafletMapEdit
key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => {
setMarkerPosition(pos);
setFormData((prev) => ({
...prev,
lat: pos.lat,
lng: pos.lng,
}));
}}
/>
</Box>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import dynamic from 'next/dynamic'
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
ssr: false
})
function DetailKeteranganBankSampahTerdekat() {
const router = useRouter();
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false)
const params = useParams()
useEffect(() => {
keteranganState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
keteranganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
}
}
if (!keteranganState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<Box
style={{
height: "300px",
}}
>
<LeafletMap
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
readOnly
/>
</Box>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }}
>
Buka Petunjuk Arah di Google Maps
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (keteranganState.findUnique.data) {
setSelectedId(keteranganState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!keteranganState.findUnique.data}
color="red"
>
<IconX size={20} />
</Button>
<Button
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)}
color="green"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?"
/>
</Box>
);
}
export default DetailKeteranganBankSampahTerdekat;

View File

@@ -1,14 +1,42 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), { ssr: false });
function CreateKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const resetForm = () => {
keteranganState.create.form = {
name: "",
alamat: "",
namaTempatMaps: "",
lat: 0,
lng: 0,
}
setMarkerPosition(null)
}
const handleSubmit = async () => {
if (markerPosition) {
keteranganState.create.form.lat = markerPosition.lat
keteranganState.create.form.lng = markerPosition.lng
}
await keteranganState.create.create()
resetForm()
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
}
return (
<Box>
<Box mb={10}>
@@ -20,22 +48,38 @@ function CreateKeteranganBankSampahTerdekat() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama bank sampah terdekat'
value={keteranganState.create.form.name}
onChange={(val) => keteranganState.create.form.name = val.target.value}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama Bank Sampah Terdekat'
/>
<TextInput
value={keteranganState.create.form.alamat}
onChange={(val) => keteranganState.create.form.alamat = val.target.value}
label={<Text fw="bold" fz="sm">Alamat</Text>}
placeholder='Masukkan alamat Bank Sampah'
/>
<TextInput
value={keteranganState.create.form.namaTempatMaps}
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
placeholder='Masukkan nama tempat maps Bank Sampah'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Bank Sampah Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
<Box style={{ height: 300, width: '100%' }}>
<LeafletMap
onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }}
/>
</Box>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
@@ -44,3 +88,4 @@ function CreateKeteranganBankSampahTerdekat() {
}
export default CreateKeteranganBankSampahTerdekat;

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailKeteranganBankSampahTerdekat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailKeteranganBankSampahTerdekat;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditKeteranganBankSampahTerdekat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama bank sampah terdekat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Bank Sampah Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKeteranganBankSampahTerdekat;

View File

@@ -1,55 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import { useEffect, useState } from 'react';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
function KeteranganBankSampahTerdekat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Keterangan Bank Sampah Terdekat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeteranganBankSampahTerdekat search={search}/>
</Box>
);
}
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
useEffect(() => {
keteranganState.findMany.load()
}, [])
const filteredData = (keteranganState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaTempatMaps.toLowerCase().includes(keyword)
);
});
if (!keteranganState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Keterangan Bank Sampah Terdekat'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Keterangan Bank Sampah Terdekat</Title>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah Terdekat</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Bank Sampah Sarana Gathi</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
<JudulList
title='List Keterangan Bank Sampah Terdekat'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah Terdekat</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Tempat Maps</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>{item.namaTempatMaps}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
)
);
}
export default KeteranganBankSampahTerdekat;

View File

@@ -19,7 +19,7 @@ type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'tr
function EditProgramKreatifDesa() {
const stateSampah = useProxy(pengelolaanSampahState)
const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({

View File

@@ -11,7 +11,7 @@ import { useProxy } from 'valtio/utils';
function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState)
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah)
const router = useRouter();
const resetForm = () => {

View File

@@ -31,7 +31,7 @@ function PengelolaanSampahBankSampah() {
}
function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const stateList = useProxy(pengelolaanSampahState)
const stateList = useProxy(pengelolaanSampahState.pengelolaanSampah)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()

View File

@@ -4,6 +4,7 @@ import pengelolaanSampahDelete from "./del";
import pengelolaanSampahFindMany from "./findMany";
import pengelolaanSampahFindUnique from "./findUnique";
import pengelolaanSampahUpdate from "./updt";
import KeteranganBankSampahTerdekat from "./keterangan-bank-sampah";
const PengelolaanSampah = new Elysia({
prefix: "/pengelolaansampah",
@@ -33,5 +34,6 @@ const PengelolaanSampah = new Elysia({
}),
}
)
.delete("/del/:id", pengelolaanSampahDelete);
.delete("/del/:id", pengelolaanSampahDelete)
.use(KeteranganBankSampahTerdekat);
export default PengelolaanSampah;

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreateKeteranganBankSampahTerdekat = {
name: string;
alamat: string;
namaTempatMaps: string;
lat: number;
lng: number;
}
export default async function keteranganBankSampahTerdekatCreate(context: Context) {
const body = context.body as FormCreateKeteranganBankSampahTerdekat;
const linkPetunjukArah = `https://www.google.com/maps/dir/?api=1&destination=${body.lat},${body.lng}`;
const created = await prisma.keteranganBankSampahTerdekat.create({
data: {
name: body.name,
alamat: body.alamat,
namaTempatMaps: body.namaTempatMaps,
lat: body.lat,
lng: body.lng,
linkPetunjukArah,
},
});
return {
success: true,
message: "Success create keterangan bank sampah terdekat",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const keteranganBankSampahTerdekatDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const keteranganBankSampahTerdekat = await prisma.keteranganBankSampahTerdekat.findUnique({
where: { id },
});
if (!keteranganBankSampahTerdekat) {
return {
status: 404,
body: "Keterangan bank sampah terdekat tidak ditemukan",
};
}
await prisma.keteranganBankSampahTerdekat.delete({
where: { id },
});
return {
success: true,
status: 200,
message: "Keterangan bank sampah terdekat berhasil dihapus",
};
};
export default keteranganBankSampahTerdekatDelete;

View File

@@ -0,0 +1,21 @@
import prisma from "@/lib/prisma";
export default async function keteranganBankSampahTerdekatFindMany() {
try {
const data = await prisma.keteranganBankSampahTerdekat.findMany({
where: { isActive: true },
});
return {
success: true,
message: "Success fetch keterangan bank sampah terdekat",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch keterangan bank sampah terdekat",
};
}
}

View File

@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keteranganBankSampahTerdekatFindUnique(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID keterangan bank sampah terdekat diperlukan",
};
}
try {
const keteranganBankSampahTerdekat = await prisma.keteranganBankSampahTerdekat.findUnique({
where: { id },
});
if (!keteranganBankSampahTerdekat) {
return {
success: false,
message: "Keterangan bank sampah terdekat tidak ditemukan",
};
}
return {
success: true,
data: keteranganBankSampahTerdekat,
};
} catch (error: any) {
console.error("Error findUnique keterangan bank sampah terdekat:", error);
return {
success: false,
message: "Gagal mengambil data keterangan bank sampah terdekat",
error: error.message,
};
}
}

View File

@@ -0,0 +1,44 @@
import Elysia, { t } from "elysia";
import keteranganBankSampahTerdekatCreate from "./create";
import keteranganBankSampahTerdekatDelete from "./del";
import keteranganBankSampahTerdekatFindMany from "./findMany";
import keteranganBankSampahTerdekatFindUnique from "./findUnique";
import keteranganBankSampahTerdekatUpdate from "./updt";
const KeteranganBankSampahTerdekat = new Elysia({
prefix: "/keteranganbankterdekat",
tags: ["Lingkungan/Pengelolaan Sampah/Keterangan Bank Sampah Terdekat"],
})
.get("/find-many", keteranganBankSampahTerdekatFindMany)
.get("/:id", async (context) => {
const response = await keteranganBankSampahTerdekatFindUnique(context);
return response;
})
.post("/create", keteranganBankSampahTerdekatCreate, {
body: t.Object({
name: t.String(),
alamat: t.String(),
namaTempatMaps: t.String(),
lat: t.Number(),
lng: t.Number(),
}),
})
.put(
"/:id",
async (context) => {
const response = await keteranganBankSampahTerdekatUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
alamat: t.String(),
namaTempatMaps: t.String(),
lat: t.Number(),
lng: t.Number(),
}),
}
)
.delete("/del/:id", keteranganBankSampahTerdekatDelete);
export default KeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdateKeteranganBankSampahTerdekat = {
id: string;
name?: string;
alamat?: string;
namaTempatMaps?: string;
lat?: number;
lng?: number;
};
export default async function keteranganBankSampahTerdekatUpdate(context: Context) {
const body = context.body as FormUpdateKeteranganBankSampahTerdekat;
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID keterangan bank sampah terdekat wajib diisi",
};
}
try {
const updateData: any = {
name: body.name,
alamat: body.alamat,
namaTempatMaps: body.namaTempatMaps,
};
if (body.lat !== undefined && body.lng !== undefined) {
updateData.lat = body.lat;
updateData.lng = body.lng;
updateData.linkPetunjukArah = `https://www.google.com/maps/dir/?api=1&destination=${body.lat},${body.lng}`;
}
const updated = await prisma.keteranganBankSampahTerdekat.update({
where: { id },
data: updateData,
});
return {
success: true,
message: "Success update keterangan bank sampah terdekat",
data: updated,
};
} catch (error) {
console.error("Update error:", error);
return {
success: false,
message: "Gagal mengupdate keterangan bank sampah terdekat",
};
}
}

View File

@@ -39,96 +39,102 @@ function InformasiDesa() {
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={10}>
{dataBerita && (
<Paper shadow="md" radius="md" p="md">
{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>
<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>
{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>
</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>
</Stack>
<Divider color={colors['blue-button']} my="md" />
<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>
<GridCol span={{ md: 6, base: 12 }}>
{dataPengumuman && (
<Paper h={"97%"} 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>
</Card>
</GridCol>
))}
</Grid>
</Paper>
)}
</GridCol>
<GridCol span={{ md: 6, base: 12 }}>
<Stack py={10}>
<Title order={3}>Pengumuman Terbaru</Title>
<Grid>
{statePengumuman.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 8, md: 6 }} 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>
</GridCol>
</Grid>
</Stack>
</Stack>
</Box>
</Stack>
);