Compare commits

...

4 Commits

123 changed files with 2477 additions and 6166 deletions

View File

@@ -221,81 +221,6 @@ model KategoriPrestasiDesa {
PrestasiDesa PrestasiDesa[]
}
//========================================= INDEKS KEPUASAN MASYARAKAT ========================================= //
// Entitas Survey
model Survey {
id String @id @default(cuid())
title String // Judul survei
totalRespondents Int // Total jumlah responden
averageScore Float // Rata-rata skor
monthlyStats MonthlyStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Statistik Bulanan
model MonthlyStat {
id String @id @default(cuid())
month String // Nama bulan (e.g., "Januari", "Februari")
respondentsCount Int // Jumlah responden per bulan
surveyId String @unique(map: "monthly_stat_survey_id_month_key")
survey Survey @relation(fields: [surveyId], references: [id])
AgeStat AgeStat[]
ResponseStat ResponseStat[]
genderStat genderStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Gender
model genderStat {
id String @id @default(cuid())
laki Int
perempuan Int
percentLaki Float
percentPerempuan Float
total Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Age
model AgeStat {
id String @id @default(cuid())
group String // "18-44", "45+" dll
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Response
model ResponseStat {
id String @id @default(cuid())
label String // BAIK / BURUK
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
@@ -732,16 +657,16 @@ model PelayananPendudukNonPermanen {
// ========================================= PENGHARGAAN ========================================= //
model Penghargaan {
id String @id @default(cuid())
id String @id @default(cuid())
name String
juara String
deskripsi String @db.Text
deskripsi String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU KESEHATAN ========================================= //

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -22,19 +23,6 @@ const defaultForm = {
kategoriBeritaId: "",
};
// 3. Kategori proxy
const category = proxy({
findMany: {
data: [] as Prisma.KategoriBeritaGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.berita.category["find-many"].get();
if (res.status === 200) {
category.findMany.data = res.data?.data ?? [];
}
},
},
});
// 4. Berita proxy
const berita = proxy({
create: {
@@ -71,6 +59,8 @@ const berita = proxy({
},
},
// State untuk berita utama (hanya 1)
findMany: {
data: null as
| Prisma.BeritaGetPayload<{
@@ -83,38 +73,43 @@ const berita = proxy({
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
berita.findMany.loading = true;
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
berita.findMany.page = page;
try {
const res = await ApiFetch.api.desa.berita["find-many"].get({
query: {
page,
limit,
},
});
berita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
} else {
berita.findMany.data = [];
berita.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
berita.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null,
data: null as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/berita/${id}`);
@@ -122,11 +117,11 @@ const berita = proxy({
const data = await res.json();
berita.findUnique.data = data.data ?? null;
} else {
console.error('Failed to fetch berita:', res.statusText);
console.error("Failed to fetch berita:", res.statusText);
berita.findUnique.data = null;
}
} catch (error) {
console.error('Error fetching berita:', error);
console.error("Error fetching berita:", error);
berita.findUnique.data = null;
}
},
@@ -140,14 +135,14 @@ const berita = proxy({
berita.delete.loading = true;
const response = await fetch(`/api/desa/berita/delete/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Berita berhasil dihapus");
await berita.findMany.load(); // refresh list
@@ -172,21 +167,21 @@ const berita = proxy({
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/berita/${id}`, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json',
"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;
@@ -203,7 +198,9 @@ const berita = proxy({
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
@@ -217,14 +214,14 @@ const berita = proxy({
toast.error(err);
return false;
}
try {
berita.edit.loading = true;
const response = await fetch(`/api/desa/berita/${this.id}`, {
method: 'PUT',
method: "PUT",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
@@ -234,14 +231,16 @@ const berita = proxy({
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update berita");
await berita.findMany.load(); // refresh list
@@ -251,7 +250,11 @@ const berita = proxy({
}
} catch (error) {
console.error("Error updating berita:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update berita");
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update berita"
);
return false;
} finally {
berita.edit.loading = false;
@@ -271,21 +274,22 @@ const berita = proxy({
};
}> | null,
loading: false,
async load() {
// findFirst.load()
async load(kategori?: string) {
this.loading = true;
try {
const res = await ApiFetch.api.desa.berita["find-first"].get();
const res = await ApiFetch.api.desa.berita["find-first"].get({
query: kategori ? { kategori } : {},
});
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;
this.data = res.data.data || null;
} else {
this.data = null;
}
} catch (err) {
console.error("Gagal fetch berita terbaru:", err);
this.data = null;
} finally {
this.loading = false;
}
@@ -299,7 +303,7 @@ const berita = proxy({
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
@@ -313,15 +317,227 @@ const berita = proxy({
this.loading = false;
}
},
}
},
});
//=============== Kategori Berita ===============
const templateKategoriBerita = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultKategoriBerita = {
name: "",
};
const kategoriBerita = proxy({
create: {
form: { ...defaultKategoriBerita },
loading: false,
async create() {
const cek = templateKategoriBerita.safeParse(kategoriBerita.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriBerita.create.loading = true;
const res = await ApiFetch.api.desa.kategoriberita["create"].post(
kategoriBerita.create.form
);
if (res.status === 200) {
kategoriBerita.findMany.load();
return toast.success("Data Kategori Berita Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
kategoriBerita.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.KategoriBeritaGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
if (res.status === 200) {
kategoriBerita.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriBeritaGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/desa/kategoriberita/${id}`);
if (res.ok) {
const data = await res.json();
kategoriBerita.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriBerita.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriBerita.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriBerita.delete.loading = true;
const response = await fetch(`/api/desa/kategoriberita/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Kategori Berita berhasil dihapus"
);
await kategoriBerita.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus Data Kategori Berita"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Kategori Berita");
} finally {
kategoriBerita.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKategoriBerita },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/kategoriberita/${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,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori berita:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKategoriBerita.safeParse(kategoriBerita.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriBerita.update.loading = true;
const response = await fetch(`/api/desa/kategoriberita/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
});
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 data kategori berita");
await kategoriBerita.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data kategori berita"
);
}
} catch (error) {
console.error("Error updating data kategori berita:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kategori berita"
);
return false;
} finally {
kategoriBerita.update.loading = false;
}
},
reset() {
kategoriBerita.update.id = "";
kategoriBerita.update.form = { ...defaultKategoriBerita };
},
},
});
// 5. State global
const stateDashboardBerita = proxy({
category,
kategoriBerita,
berita,
});

View File

@@ -1,10 +1,222 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateKategoriPengumuman = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultKategoriPengumuman = {
name: "",
};
const category = proxy({
create: {
form: { ...defaultKategoriPengumuman },
loading: false,
async create() {
const cek = templateKategoriPengumuman.safeParse(category.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
category.create.loading = true;
const res = await ApiFetch.api.desa.kategoripengumuman["create"].post(
category.create.form
);
if (res.status === 200) {
category.findMany.load();
return toast.success("Data Kategori Pengumuman Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
category.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.CategoryPengumumanGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
if (res.status === 200) {
category.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.CategoryPengumumanGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/desa/kategoripengumuman/${id}`);
if (res.ok) {
const data = await res.json();
category.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
category.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
category.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
category.delete.loading = true;
const response = await fetch(`/api/desa/kategoripengumuman/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Kategori Pengumuman berhasil dihapus"
);
await category.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus Data Kategori Pengumuman"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Kategori Pengumuman");
} finally {
category.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKategoriPengumuman },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/kategoripengumuman/${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,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori berita:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKategoriPengumuman.safeParse(category.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
category.update.loading = true;
const response = await fetch(`/api/desa/kategoripengumuman/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
});
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 data kategori pengumuman");
await category.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data kategori pengumuman"
);
}
} catch (error) {
console.error("Error updating data kategori pengumuman:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kategori pengumuman"
);
return false;
} finally {
category.update.loading = false;
}
},
reset() {
category.update.id = "";
category.update.form = { ...defaultKategoriPengumuman };
},
},
});
const templateFormPengumuman = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
@@ -12,22 +224,6 @@ const templateFormPengumuman = z.object({
categoryPengumumanId: z.string().nonempty(),
});
const category = proxy({
findMany: {
data: null as
| null
| Prisma.CategoryPengumumanGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.pengumuman.category[
"find-many"
].get();
if (res.status === 200) {
category.findMany.data = (res.data?.data as any) ?? [];
}
},
},
});
type PengumumanForm = Prisma.PengumumanGetPayload<{
select: {
judul: true;

View File

@@ -5,25 +5,21 @@ import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Survey",
value: "listSurvey",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"
label: "List Berita",
value: "list_berita",
href: "/admin/desa/berita/list-berita"
},
{
label: "List Bulanan",
value: "listBulanan",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan"
label: "Kategori Berita",
value: "kategori_berita",
href: "/admin/desa/berita/kategori-berita"
},
{
label: "List Gender Stat",
value: "listGenderStat",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
@@ -45,7 +41,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return (
<Stack>
<Title order={3}>Indeks Kepuasan Masyarakat</Title>
<Title order={3}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
@@ -64,4 +60,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
);
}
export default LayoutTabs;
export default LayoutTabsBerita;

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Berita:", error);
toast.error("Gagal memuat data kategori Berita");
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Berita berhasil diperbarui!');
router.push('/admin/desa/berita/kategori-berita');
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kategori Berita</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
placeholder="masukkan nama kategori Berita"
/>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriBerita;

View File

@@ -0,0 +1,55 @@
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita)
const router = useRouter();
const resetForm = () => {
createState.create.form = {
name: "",
};
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push("/admin/desa/berita/kategori-berita")
};
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}>Create Kategori Berita</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
placeholder='Masukkan nama kategori Berita'
value={createState.create.form.name}
onChange={(val) => {
createState.create.form.name = val.target.value;
}}
/>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKategoriBerita;

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita';
function KategoriBerita() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kategori Berita'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriBerita search={search} />
</Box>
);
}
function ListKategoriBerita({ search }: { search: string }) {
const listDataState = useProxy(stateDashboardBerita.kategoriBerita)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
listDataState.findMany.load()
}, [])
const handleDelete = () => {
if (selectedId) {
listDataState.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
listDataState.findMany.load()
}
}
const filteredData = (listDataState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Kategori Berita'
href='/admin/desa/berita/kategori-berita/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus kategori Berita ini?'
/>
</Box>
)
}
export default KategoriBerita;

View File

@@ -0,0 +1,13 @@
'use client'
import React from 'react';
import LayoutTabsBerita from './_com/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsBerita>
{children}
</LayoutTabsBerita>
);
}
export default Layout;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
@@ -7,7 +8,6 @@ import {
Image,
Paper,
Select,
Skeleton,
Stack,
Text,
TextInput,
@@ -22,9 +22,8 @@ import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import stateDashboardBerita from "../../../../_state/desa/berita";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
function EditBerita() {
const beritaState = useProxy(stateDashboardBerita);
@@ -43,6 +42,7 @@ function EditBerita() {
// Load berita by id saat pertama kali
useEffect(() => {
beritaState.kategoriBerita.findMany.load()
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
@@ -99,7 +99,7 @@ function EditBerita() {
await beritaState.berita.edit.update();
toast.success("Berita berhasil diperbarui!");
router.push("/admin/desa/berita");
router.push("/admin/desa/berita/list-berita");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
@@ -154,22 +154,29 @@ function EditBerita() {
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
beritaState.berita.edit.form.content = htmlContent;
}}
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
beritaState.berita.edit.form.content = htmlContent;
}}
/>
</Box>
<SelectCategory
<Select
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder='Pilih kategori'
data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id,
label: v.name
})) || []
}
clearable
searchable
required
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/>
<Button onClick={handleSubmit}>Edit Berita</Button>
@@ -179,61 +186,4 @@ function EditBerita() {
);
}
interface SelectCategoryProps {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}> | null) => void;
value?: string | null;
defaultValue?: string | null;
}
function SelectCategory({
onChange,
value,
defaultValue,
}: SelectCategoryProps) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load().then(() => {
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
});
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="Pilih kategori"
data={categoryState.findMany.data.map((item) => ({
label: item.name,
value: item.id,
}))}
value={selectedValue || null}
onChange={(val: string | null) => {
if (val) {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
} else {
onChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
export default EditBerita;

View File

@@ -8,8 +8,8 @@ import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita)
@@ -28,7 +28,7 @@ function DetailBerita() {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/berita")
router.push("/admin/desa/berita/list-berita")
}
}
@@ -89,7 +89,7 @@ function DetailBerita() {
<Button
onClick={() => {
if (beritaState.berita.findUnique.data) {
router.push(`/admin/desa/berita/${beritaState.berita.findUnique.data.id}/edit`);
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
}
}}
disabled={!beritaState.berita.findUnique.data}

View File

@@ -1,16 +1,16 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Center, FileInput, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import stateDashboardBerita from '../../../_state/desa/berita';
export default function CreateBerita() {
const beritaState = useProxy(stateDashboardBerita);
@@ -18,6 +18,10 @@ export default function CreateBerita() {
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load()
}, []);
const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = {
@@ -57,7 +61,7 @@ export default function CreateBerita() {
// Reset form setelah submit
resetForm();
router.push("/admin/desa/berita")
router.push("/admin/desa/berita/list-berita")
};
return (
@@ -78,11 +82,27 @@ export default function CreateBerita() {
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<SelectCategory
value={beritaState.berita.create.form.kategoriBeritaId}
onChange={(val) => {
beritaState.berita.create.form.kategoriBeritaId = val?.id || "";
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="Pilih kategori"
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
label: item.name,
value: item.id,
}))}
value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => {
if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val);
if (selected) {
beritaState.berita.create.form.kategoriBeritaId = selected.id;
}
} else {
beritaState.berita.create.form.kategoriBeritaId = "";
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
<TextInput
value={beritaState.berita.create.form.deskripsi}
@@ -126,61 +146,4 @@ export default function CreateBerita() {
</Paper>
</Box>
);
interface SelectCategoryProps {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}> | null) => void;
value?: string | null;
defaultValue?: string | null;
}
function SelectCategory({
onChange,
value,
defaultValue,
}: SelectCategoryProps) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load().then(() => {
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
});
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="Pilih kategori"
data={categoryState.findMany.data.map((item) => ({
label: item.name,
value: item.id,
}))}
value={selectedValue || null}
onChange={(val: string | null) => {
if (val) {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
} else {
onChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
}

View File

@@ -6,8 +6,9 @@ import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/ico
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import stateDashboardBerita from '../../_state/desa/berita';
import HeaderSearch from '../../../_com/header';
import stateDashboardBerita from '../../../_state/desa/berita';
function Berita() {
@@ -38,22 +39,16 @@ function ListBerita({ search }: { search: string }) {
} = beritaState.berita.findMany;
// Fetch pertama kali
// Fetch data when page or search changes
useShallowEffect(() => {
load(page, 10); // awal page = 1
}, [page]);
const filteredData = (data || []).filter((item) => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.kategoriBerita?.name.toLowerCase().includes(keyword)
);
});
load(page, 10, search);
}, [page, search]);
if (loading || !data) {
return <Skeleton h={500} />;
}
const filteredData = data || [];
return (
<Box py={10}>
@@ -67,7 +62,7 @@ function ListBerita({ search }: { search: string }) {
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
onClick={() => router.push("/admin/desa/berita/create")}
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
bg={colors["blue-button"]}
>
<IconCircleDashedPlus size={25} />
@@ -107,7 +102,7 @@ function ListBerita({ search }: { search: string }) {
<Button
bg={"green"}
onClick={() =>
router.push(`/admin/desa/berita/${item.id}`)
router.push(`/admin/desa/berita/list-berita/${item.id}`)
}
>
<IconDeviceImacCog size={25} />

View File

@@ -0,0 +1,62 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Pengumuman",
value: "listpengumuman",
href: "/admin/desa/pengumuman/list-pengumuman"
},
{
label: "Kategori Pengumuman",
value: "kategoripengumuman",
href: "/admin/desa/pengumuman/kategori-pengumuman"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Pengumuman</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsLayanan;

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Pengumuman:", error);
toast.error("Gagal memuat data kategori Pengumuman");
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Pengumuman berhasil diperbarui!');
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kategori Pengumuman</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>}
placeholder="masukkan nama kategori Pengumuman"
/>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriPengumuman;

View File

@@ -0,0 +1,55 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category)
const router = useRouter();
const resetForm = () => {
createState.create.form = {
name: "",
};
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push("/admin/desa/pengumuman/kategori-pengumuman")
};
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}>Create Kategori Pengumuman</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>}
placeholder='Masukkan nama kategori Pengumuman'
value={createState.create.form.name}
onChange={(val) => {
createState.create.form.name = val.target.value;
}}
/>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKategoriPengumuman;

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function KategoriPengumuman() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kategori Pengumuman'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriPengumuman search={search} />
</Box>
);
}
function ListKategoriPengumuman({ search }: { search: string }) {
const listDataState = useProxy(stateDesaPengumuman.category)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
listDataState.findMany.load()
}, [])
const handleDelete = () => {
if (selectedId) {
listDataState.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
listDataState.findMany.load()
}
}
const filteredData = (listDataState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Kategori Pengumuman'
href='/admin/desa/pengumuman/kategori-pengumuman/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?'
/>
</Box>
)
}
export default KategoriPengumuman;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
import LayoutTabs from './_com/layoutTabs';
function Layout({children} : {children: React.ReactNode}) {
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}

View File

@@ -1,4 +1,6 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
@@ -6,8 +8,7 @@ import { Prisma } from '@prisma/client';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman)

View File

@@ -5,10 +5,10 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import stateDesaPengumuman from '../../_state/desa/pengumuman';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function Pengumuman() {
@@ -16,7 +16,7 @@ function Pengumuman() {
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
title='List Pengumuman'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
@@ -113,7 +113,7 @@ function ListPengumuman({ search }: { search: string }) {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
text='Apakah anda yakin ingin menghapus pengumuman ini?'
/>
</Box>
)

View File

@@ -1,128 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListBulanan() {
const editState = useProxy(indeksKepuasanState.monthlyStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
month: editState.edit.form.month || '',
respondentsCount: editState.edit.form.respondentsCount || 0,
surveyId: editState.edit.form.surveyId || '',
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
month: data.month,
respondentsCount: data.respondentsCount,
surveyId: data.surveyId,
});
}
} catch (error) {
console.error("Error loading list bulanan:", error);
toast.error("Gagal memuat data list bulanan");
}
};
indeksKepuasanState.surveyState.findMany.load();
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
month: formData.month,
respondentsCount: formData.respondentsCount,
surveyId: formData.surveyId,
};
await editState.edit.update();
toast.success("list bulanan berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan");
} catch (error) {
console.error("Error updating list bulanan:", error);
toast.error("Terjadi kesalahan saat memperbarui list bulanan");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.month}
onChange={(val) => {
setFormData({
...formData,
month: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={formData.respondentsCount}
onChange={(val) => {
setFormData({
...formData,
respondentsCount: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={formData.surveyId}
onChange={(value) => {
if (value) setFormData({
...formData,
surveyId: value
})
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListBulanan;

View File

@@ -1,109 +0,0 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailListBulanan() {
const detailState = useProxy(indeksKepuasanState.monthlyStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bulan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.month}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.respondentsCount}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Survey</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.survey.title }</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list bulanan ini?'
/>
</Box>
);
}
export default DetailListBulanan;

View File

@@ -1,84 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListBulanan() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.monthlyStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.surveyState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
month: "",
respondentsCount: 0,
surveyId: "",
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
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}>Create List Bulanan</Title>
<TextInput
value={stateCreate.create.form.month}
onChange={(val) => {
stateCreate.create.form.month = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={stateCreate.create.form.respondentsCount}
onChange={(val) => {
stateCreate.create.form.respondentsCount = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={stateCreate.create.form.surveyId}
onChange={(value) => {
if (value) stateCreate.create.form.surveyId = value;
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListBulanan;

View File

@@ -1,98 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListBulananLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Bulanan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBUlanan search={search} />
</Box>
);
}
function ListBUlanan({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.monthlyStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.month.toLowerCase().includes(keyword) ||
item.respondentsCount.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Bulanan'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.month}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.respondentsCount}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListBulananLandingPage;

View File

@@ -1,186 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListSurvey() {
const editState = useProxy(indeksKepuasanState.genderStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
laki: editState.edit.form.laki || 0,
perempuan: editState.edit.form.perempuan || 0,
monthlyStatId: editState.edit.form.monthlyStatId || '',
total: editState.edit.form.total || 0,
percentLaki: editState.edit.form.percentLaki || 0,
percentPerempuan: editState.edit.form.percentPerempuan || 0,
})
useEffect(() => {
indeksKepuasanState.monthlyStatState.findMany.load();
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
laki: data.laki || 0,
perempuan: data.perempuan || 0,
monthlyStatId: data.monthlyStatId || '',
total: data.total || 0,
percentLaki: data.percentLaki || 0,
percentPerempuan: data.percentPerempuan || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
// Hitung total dan persentase saat nilai laki atau perempuan berubah
useEffect(() => {
const total = formData.laki + formData.perempuan;
const percentLaki = total > 0 ? Math.round((formData.laki / total) * 100) : 0;
const percentPerempuan = 100 - percentLaki;
setFormData(prev => ({
...prev,
total,
percentLaki,
percentPerempuan
}));
}, [formData.laki, formData.perempuan]);
const handleSubmit = async () => {
if (!formData.monthlyStatId) {
return toast.error("Silakan pilih bulan terlebih dahulu");
}
if (formData.laki < 0 || formData.perempuan < 0) {
return toast.error("Nilai tidak boleh negatif");
}
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
laki: formData.laki,
perempuan: formData.perempuan,
monthlyStatId: formData.monthlyStatId,
total: formData.total,
percentLaki: formData.percentLaki,
percentPerempuan: formData.percentPerempuan,
};
await editState.edit.update();
toast.success("Data gender berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat");
} catch (error) {
console.error("Error updating list gender:", error);
toast.error("Terjadi kesalahan saat memperbarui data gender");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
type="number"
value={formData.laki}
onChange={(val) => {
setFormData({
...formData,
laki: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Laki - Laki</Text>}
placeholder='Masukkan laki - laki'
/>
<TextInput
type="number"
value={formData.perempuan}
onChange={(val) => {
setFormData({
...formData,
perempuan: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
type="number"
value={formData.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={"Pilih Bulan"}
placeholder="Pilih bulanan"
value={formData.monthlyStatId}
onChange={(value) => {
setFormData(prev => ({
...prev,
monthlyStatId: value || ''
}));
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
required
error={!formData.monthlyStatId ? 'Bulan harus dipilih' : undefined}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={editState.edit.loading}
>
{editState.edit.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -1,113 +0,0 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailGenderStat() {
const detailState = useProxy(indeksKepuasanState.genderStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.laki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.perempuan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentLaki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentPerempuan}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list gender stat ini?'
/>
</Box>
);
}
export default DetailGenderStat;

View File

@@ -1,103 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListGenderStat() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.genderStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.monthlyStatState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
laki: 0,
perempuan: 0,
monthlyStatId: "",
total: 0,
percentLaki: 0,
percentPerempuan: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
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}>Create List Gender Stat</Title>
<TextInput
value={stateCreate.create.form.laki}
onChange={(val) => {
stateCreate.create.form.laki = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Laki Laki</Text>}
placeholder='Masukkan laki laki'
/>
<TextInput
value={stateCreate.create.form.perempuan}
onChange={(val) => {
stateCreate.create.form.perempuan = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
value={stateCreate.create.form.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Bulan</Text>}
placeholder="Pilih bulanan"
value={stateCreate.create.form.monthlyStatId}
onChange={(value) => {
if (value) stateCreate.create.form.monthlyStatId = value;
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListGenderStat;

View File

@@ -1,106 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
function ListGenderStat() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Gender Stat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGender search={search} />
</Box>
);
}
function ListGender({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.genderStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().includes(keyword) ||
item.perempuan.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Gender Stat'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Laki Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Persentase Laki Laki</TableTh>
<TableTh>Persentase Perempuan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.laki}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.perempuan}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentLaki}%</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentPerempuan}%</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListGenderStat;

View File

@@ -1,122 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListSurvey() {
const editState = useProxy(indeksKepuasanState.surveyState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
title: editState.edit.form.title || '',
totalRespondents: editState.edit.form.totalRespondents || 0,
averageScore: editState.edit.form.averageScore || 0,
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
title: data.title || '',
totalRespondents: data.totalRespondents || 0,
averageScore: data.averageScore || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
title: formData.title,
totalRespondents: formData.totalRespondents,
averageScore: formData.averageScore,
};
await editState.edit.update();
toast.success("list survey berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey");
} catch (error) {
console.error("Error updating list survey:", error);
toast.error("Terjadi kesalahan saat memperbarui list survey");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.title}
onChange={(val) => {
setFormData({
...formData,
title: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={formData.totalRespondents}
onChange={(val) => {
setFormData({
...formData,
totalRespondents: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<TextInput
value={formData.averageScore}
onChange={(val) => {
setFormData({
...formData,
averageScore: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Skor Rata-rata</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -1,109 +0,0 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailKegiatanDesa() {
const detailState = useProxy(indeksKepuasanState.surveyState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.title}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.totalRespondents}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Rata-rata Skor</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.averageScore}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list survey ini?'
/>
</Box>
);
}
export default DetailKegiatanDesa;

View File

@@ -1,78 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListSurvey() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.surveyState)
useEffect(() => {
stateCreate.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
title: "",
totalRespondents: 0,
averageScore: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
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}>Create List Survey</Title>
<TextInput
value={stateCreate.create.form.title}
onChange={(val) => {
stateCreate.create.form.title = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={stateCreate.create.form.totalRespondents}
onChange={(val) => {
stateCreate.create.form.totalRespondents = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
value={stateCreate.create.form.averageScore}
onChange={(val) => {
stateCreate.create.form.averageScore = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Skor</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListSurvey;

View File

@@ -1,103 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListSurveyLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Survey'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSurvey search={search} />
</Box>
);
}
function ListSurvey({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.surveyState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.title.toLowerCase().includes(keyword) ||
item.totalRespondents.toString().includes(keyword) ||
item.averageScore.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Survey'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Skor Rata-rata</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.title}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.totalRespondents}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.averageScore}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListSurveyLandingPage;

View File

@@ -103,12 +103,12 @@ export const navBar = [
{
id: "Desa_3",
name: "Berita",
path: "/admin/desa/berita"
path: "/admin/desa/berita/list-berita"
},
{
id: "Desa_4",
name: "Pengumuman",
path: "/admin/desa/pengumuman"
path: "/admin/desa/pengumuman/list-pengumuman"
},
{
id: "Desa_5",

View File

@@ -1,9 +0,0 @@
import prisma from "@/lib/prisma";
async function kategoriBeritaFindMany() {
const data = await prisma.kategoriBerita.findMany();
return { data };
}
export default kategoriBeritaFindMany

View File

@@ -1,44 +1,71 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function beritaFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const kategori = (context.query.kategori as string) || ''; // 🔥 Parameter kategori baru
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Filter berdasarkan kategori (jika ada)
if (kategori) {
where.kategoriBerita = {
name: {
equals: kategori,
mode: 'insensitive' // Tidak case-sensitive
}
};
}
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ judul: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
{ kategoriBerita: { name: { contains: search, mode: 'insensitive' } } }
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.berita.findMany({
where: { isActive: true },
where,
include: {
image: true,
kategoriBerita: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
orderBy: { createdAt: 'desc' },
}),
prisma.berita.count({
where: { isActive: true }
})
prisma.berita.count({ where }),
]);
return {
success: true,
message: "Success fetch berita with pagination",
message: "Berhasil ambil berita dengan pagination",
data,
page,
totalPages: Math.ceil(total / limit),
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Find many paginated error:", e);
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Failed fetch berita with pagination",
message: "Gagal mengambil data berita",
};
}
}
export default beritaFindMany;
export default beritaFindMany;

View File

@@ -1,30 +1,41 @@
import prisma from '@/lib/prisma';
/* eslint-disable @typescript-eslint/no-explicit-any */
// find-first.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function beritaFindFirst(context: Context) {
const kategori = (context.query.kategori as string) || '';
const where: any = { isActive: true };
if (kategori) {
where.kategoriBerita = {
name: { equals: kategori, mode: 'insensitive' }
};
}
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
},
const data = await prisma.berita.findFirst({
where,
include: {
image: true,
kategoriBerita: true,
}
},
orderBy: { createdAt: 'desc' },
});
return {
success: true,
message: 'Berhasil ambil berita terbaru',
data: result,
message: "Berhasil ambil berita terbaru",
data,
};
} catch (error) {
console.error('[findFirstBerita] Error:', error);
} catch (e) {
console.error("Error di findFirst:", e);
return {
success: false,
message: 'Gagal ambil berita terbaru',
message: "Gagal ambil berita terbaru",
};
}
}
export default beritaFindFirst;

View File

@@ -0,0 +1,14 @@
import prisma from "@/lib/prisma";
export default async function findManyUI() {
const data = await prisma.berita.findMany({
include: {
image: true,
kategoriBerita: true,
},
});
return {
success: true,
data,
};
}

View File

@@ -1,5 +1,4 @@
import Elysia, { t } from "elysia";
import kategoriBeritaFindMany from "./category";
import beritaFindMany from "./find-many";
import beritaCreate from "./create";
import beritaDelete from "./del";
@@ -9,8 +8,8 @@ import beritaFindFirst from "./findFirst";
import findRecentBerita from "./findRecent";
const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
.get("/category/find-many", kategoriBeritaFindMany)
.get("/find-many", beritaFindMany)
.get("/find-many-ui", beritaFindMany)
.get("/:id", async (context) => {
const response = await findBeritaById(new Request(context.request));
return response;

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
}
export default async function kategoriBeritaCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.kategoriBerita.create({
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil membuat kategori berita",
data: result,
};
} catch (error) {
console.error("Error creating kategori berita:", error);
throw new Error("Gagal membuat kategori berita: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriBeritaDelete(context: Context) {
const id = context.params.id as string;
await prisma.kategoriBerita.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete kategori berita",
};
}

View File

@@ -0,0 +1,11 @@
import prisma from "@/lib/prisma";
export default async function kategoriBeritaFindMany() {
const data = await prisma.kategoriBerita.findMany();
return {
success: true,
message: "Success get all kategori berita",
data,
};
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function kategoriBeritaFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.kategoriBerita.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get kategori berita",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import kategoriBeritaCreate from "./create";
import kategoriBeritaDelete from "./del";
import kategoriBeritaFindMany from "./findMany";
import kategoriBeritaFindUnique from "./findUnique";
import kategoriBeritaUpdate from "./updt";
const KategoriBerita = new Elysia({
prefix: "/kategoriberita",
tags: ["Desa / Berita / Kategori Berita"],
})
.post("/create", kategoriBeritaCreate, {
body: t.Object({
name: t.String(),
}),
})
.get("/findMany", kategoriBeritaFindMany)
.get("/:id", async (context) => {
const response = await kategoriBeritaFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", kategoriBeritaUpdate, {
body: t.Object({
name: t.String(),
}),
})
.delete("/del/:id", kategoriBeritaDelete);
export default KategoriBerita;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
}
export default async function kategoriBeritaUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.kategoriBerita.update({
where: { id },
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil mengupdate kategori berita",
data: result,
};
} catch (error) {
console.error("Error updating kategori berita:", error);
throw new Error("Gagal mengupdate kategori berita: " + (error as Error).message);
}
}

View File

@@ -8,6 +8,8 @@ import GalleryVideo from "./gallery/video";
import LayananDesa from "./layanan";
import Penghargaan from "./penghargaan";
import KategoriPotensi from "./potensi/kategori-potensi";
import KategoriBerita from "./berita/kategori-berita";
import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
@@ -20,5 +22,7 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(LayananDesa)
.use(Penghargaan)
.use(KategoriPotensi)
.use(KategoriBerita)
.use(KategoriPengumuman)
export default Desa;

View File

@@ -1,16 +1,13 @@
import Elysia from "elysia";
import Elysia, { t } from "elysia";
import { pengumumanCreate } from "./create";
import pengumumanFindMany from "./find-many";
import { t } from "elysia";
import pengumumanCategoryFindMany from "./category";
import pengumumanDelete from "./del";
import pengumumanFindById from "./find-by-id";
import pengumumanUpdate from "./updt";
import pengumumanFindMany from "./find-many";
import pengumumanFindFirst from "./findFirst";
import pengumumanFindRecent from "./findRecent";
import pengumumanUpdate from "./updt";
const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] })
.get("/category/find-many", pengumumanCategoryFindMany)
.get("/find-many", pengumumanFindMany)
.get("/:id", pengumumanFindById)
.delete("/delete/:id", pengumumanDelete)

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
}
export default async function kategoriPengumumanCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.categoryPengumuman.create({
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil membuat kategori pengumuman",
data: result,
};
} catch (error) {
console.error("Error creating kategori pengumuman:", error);
throw new Error("Gagal membuat kategori pengumuman: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriPengumumanDelete(context: Context) {
const id = context.params.id as string;
await prisma.categoryPengumuman.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete kategori pengumuman",
};
}

View File

@@ -0,0 +1,8 @@
import prisma from "@/lib/prisma";
async function kategoriPengumumanFindMany() {
const data = await prisma.categoryPengumuman.findMany();
return { data };
}
export default kategoriPengumumanFindMany

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function kategoriPengumumanFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.categoryPengumuman.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get kategori pengumuman",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import kategoriPengumumanCreate from "./create";
import kategoriPengumumanDelete from "./del";
import kategoriPengumumanFindMany from "./findMany";
import kategoriPengumumanFindUnique from "./findUnique";
import kategoriPengumumanUpdate from "./updt";
const KategoriPengumuman = new Elysia({
prefix: "/kategoripengumuman",
tags: ["Desa / Pengumuman / Kategori Pengumuman"],
})
.post("/create", kategoriPengumumanCreate, {
body: t.Object({
name: t.String(),
}),
})
.get("/findMany", kategoriPengumumanFindMany)
.get("/:id", async (context) => {
const response = await kategoriPengumumanFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", kategoriPengumumanUpdate, {
body: t.Object({
name: t.String(),
}),
})
.delete("/del/:id", kategoriPengumumanDelete);
export default KategoriPengumuman;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
}
export default async function kategoriPengumumanUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.categoryPengumuman.update({
where: { id },
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil mengupdate kategori pengumuman",
data: result,
};
} catch (error) {
console.error("Error updating kategori pengumuman:", error);
throw new Error("Gagal mengupdate kategori pengumuman: " + (error as Error).message);
}
}

View File

@@ -1,33 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const ageStatCreate = async (context: Context) => {
const body = (await context.body) as {
group: string;
count: number;
monthlyStatId: string;
};
try {
const created = await prisma.ageStat.create({
data: {
group: body.group,
count: body.count,
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Age Stat berhasil dibuat",
data: created,
};
} catch (error) {
console.error("Gagal create age stat:", error);
return {
success: false,
message: "Gagal membuat data age stat",
};
}
};
export default ageStatCreate;

View File

@@ -1,24 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const ageStatDelete = async (context: Context) => {
const { id } = context.params as { id: string };
try {
await prisma.ageStat.delete({
where: { id },
});
return {
success: true,
message: "Data age berhasil dihapus",
};
} catch (error) {
console.error("Gagal hapus Age Stat:", error);
return {
success: false,
message: "Gagal menghapus data age stat",
};
}
};
export default ageStatDelete;

View File

@@ -1,26 +0,0 @@
import prisma from "@/lib/prisma";
export const ageStatFindMany = async () => {
try {
const result = await prisma.ageStat.findMany({
include: {
MonthlyStat: true,
},
orderBy: {
id: "desc",
},
});
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Age Stat:", error);
return {
success: false,
message: "Gagal mengambil data age stat",
};
}
};
export default ageStatFindMany;

View File

@@ -1,34 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const ageStatFindUnique = async (context: Context) => {
const { id } = context.params as { id: string };
try {
const result = await prisma.ageStat.findUnique({
where: { id },
include: {
MonthlyStat: true,
},
});
if (!result) {
return {
success: false,
message: "Data age tidak ditemukan",
};
}
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Age Stat:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data age stat",
};
}
};
export default ageStatFindUnique;

View File

@@ -1,31 +0,0 @@
import Elysia, { t } from "elysia";
import ageStatCreate from "./create";
import ageStatDelete from "./del";
import ageStatFindMany from "./findMany";
import ageStatFindUnique from "./findUnique";
import ageStatUpdate from "./updt";
const age = new Elysia({
prefix: "/age",
tags: ["Landing Page/Indeks Kepuasan Masyarakat/Age"],
})
.post("/create", ageStatCreate, {
body: t.Object({
monthlyStatId: t.String(),
group: t.String(),
count: t.Number(),
}),
})
.get("/findMany", ageStatFindMany)
.get("/:id", ageStatFindUnique)
.put("/:id", ageStatUpdate, {
body: t.Object({
group: t.String(),
count: t.Number(),
monthlyStatId: t.String(),
}),
})
.delete("/del/:id", ageStatDelete);
export default age;

View File

@@ -1,35 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const ageStatUpdate = async (context: Context) => {
const { id } = context.params as { id: string };
const body = (await context.body) as {
group?: string;
count?: number;
monthlyStatId?: string;
};
try {
const updated = await prisma.ageStat.update({
where: { id },
data: {
group: body.group,
count: body.count,
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Data age berhasil diperbarui",
data: updated,
};
} catch (error) {
console.error("Gagal update Age Stat:", error);
return {
success: false,
message: "Gagal memperbarui data age stat",
};
}
};
export default ageStatUpdate;

View File

@@ -1,40 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const genderStatCreate = async (context: Context) => {
const body = (await context.body) as {
laki: number;
perempuan: number;
monthlyStatId: string;
};
const total = body.laki + body.perempuan;
const percentLaki = total > 0 ? Number(((body.laki / total) * 100).toFixed(2)) : 0;
const percentPerempuan = total > 0 ? Number(((body.perempuan / total) * 100).toFixed(2)) : 0;
try {
const created = await prisma.genderStat.create({
data: {
laki: body.laki,
perempuan: body.perempuan,
total,
percentLaki: Number(percentLaki),
percentPerempuan: Number(percentPerempuan),
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Gender Stat berhasil dibuat",
data: created,
};
} catch (error) {
console.error("Gagal create gender stat:", error);
return {
success: false,
message: "Gagal membuat data gender stat",
};
}
};
export default genderStatCreate;

View File

@@ -1,24 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const genderStatDelete = async (context: Context) => {
const { id } = context.params as { id: string };
try {
await prisma.genderStat.delete({
where: { id },
});
return {
success: true,
message: "Data gender berhasil dihapus",
};
} catch (error) {
console.error("Gagal hapus Gender Stat:", error);
return {
success: false,
message: "Gagal menghapus data gender",
};
}
};
export default genderStatDelete;

View File

@@ -1,26 +0,0 @@
import prisma from "@/lib/prisma";
export const genderStatFindMany = async () => {
try {
const result = await prisma.genderStat.findMany({
include: {
MonthlyStat: true,
},
orderBy: {
id: "desc",
},
});
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Gender Stat:", error);
return {
success: false,
message: "Gagal mengambil data gender stat",
};
}
};
export default genderStatFindMany;

View File

@@ -1,34 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const genderStatFindUnique = async (context: Context) => {
const { id } = context.params as { id: string };
try {
const result = await prisma.genderStat.findUnique({
where: { id },
include: {
MonthlyStat: true,
},
});
if (!result) {
return {
success: false,
message: "Data gender tidak ditemukan",
};
}
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Gender Stat:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data gender stat",
};
}
};
export default genderStatFindUnique;

View File

@@ -1,37 +0,0 @@
import Elysia, { t } from "elysia";
import genderStatCreate from "./create";
import genderStatDelete from "./del";
import genderStatFindMany from "./findMany";
import genderStatFindUnique from "./findUnique";
import genderStatUpdate from "./updt";
const gender = new Elysia({
prefix: "/gender",
tags: ["Landing Page/Indeks Kepuasan Masyarakat/Gender"],
})
.post("/create", genderStatCreate, {
body: t.Object({
monthlyStatId: t.String(),
laki: t.Number(),
perempuan: t.Number(),
total: t.Number(),
percentLaki: t.Number(),
percentPerempuan: t.Number(),
}),
})
.get("/findMany", genderStatFindMany)
.get("/:id", genderStatFindUnique)
.put("/:id", genderStatUpdate, {
body: t.Object({
laki: t.Number(),
perempuan: t.Number(),
monthlyStatId: t.String(),
total: t.Number(),
percentLaki: t.Number(),
percentPerempuan: t.Number(),
}),
})
.delete("/del/:id", genderStatDelete);
export default gender;

View File

@@ -1,42 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const genderStatUpdate = async (context: Context) => {
const { id } = context.params as { id: string };
const body = (await context.body) as {
laki?: number;
perempuan?: number;
monthlyStatId?: string;
};
const total = (body.laki ?? 0) + (body.perempuan ?? 0);
const percentLaki = total > 0 ? Number(((body.laki ?? 0) / total) * 100).toFixed(2) : 0;
const percentPerempuan = total > 0 ? Number(((body.perempuan ?? 0) / total) * 100).toFixed(2) : 0;
try {
const updated = await prisma.genderStat.update({
where: { id },
data: {
laki: body.laki,
perempuan: body.perempuan,
total,
percentLaki: Number(percentLaki),
percentPerempuan: Number(percentPerempuan),
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Data gender berhasil diperbarui",
data: updated,
};
} catch (error) {
console.error("Gagal update Gender Stat:", error);
return {
success: false,
message: "Gagal memperbarui data gender stat",
};
}
};
export default genderStatUpdate;

View File

@@ -1,20 +0,0 @@
import Elysia from "elysia";
import gender from "./gender";
import monthlyStat from "./monthly-stat";
import survey from "./survey";
import response from "./response";
import age from "./age";
const IndeksKepuasanMasyarakat = new Elysia({
prefix: "/indekskepuasanmasyarakat",
tags: ["Landing Page/Profile/Indeks Kepuasan Masyarakat"],
})
.use(gender)
.use(monthlyStat)
.use(survey)
.use(response)
.use(age)
export default IndeksKepuasanMasyarakat

View File

@@ -1,33 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const createMonthlyStat = async (context: Context) => {
const body = await context.body as {
month: string;
respondentsCount: number;
surveyId: string;
};
try {
const created = await prisma.monthlyStat.create({
data: {
month: body.month,
respondentsCount: body.respondentsCount,
surveyId: body.surveyId,
},
});
return {
success: true,
message: "Monthly stat berhasil dibuat",
data: created,
};
} catch (error) {
console.error("Gagal create monthly stat:", error);
return {
success: false,
message: "Gagal membuat monthly stat",
};
}
};
export default createMonthlyStat;

View File

@@ -1,26 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const deleteMonthlyStat = async (context: Context) => {
const { id } = context.params as { id: string };
try {
await prisma.monthlyStat.delete({
where: { id },
});
return {
success: true,
message: "Data statistik bulanan berhasil dihapus",
};
} catch (error) {
console.error("Gagal hapus MonthlyStat:", error);
return {
success: false,
message: "Gagal menghapus data statistik bulanan",
};
}
};
export default deleteMonthlyStat;

View File

@@ -1,26 +0,0 @@
import prisma from "@/lib/prisma";
export const findManyMonthlyStat = async () => {
try {
const result = await prisma.monthlyStat.findMany({
include: {
survey: true,
},
orderBy: {
id: "desc",
},
});
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data MonthlyStat:", error);
return {
success: false,
message: "Gagal mengambil data statistik bulanan",
};
}
};
export default findManyMonthlyStat;

View File

@@ -1,35 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const findUniqueMonthlyStat = async (context: Context) => {
const { id } = context.params as { id: string };
try {
const result = await prisma.monthlyStat.findUnique({
where: { id },
include: {
survey: true,
},
});
if (!result) {
return {
success: false,
message: "Data tidak ditemukan",
};
}
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data MonthlyStat:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data",
};
}
};
export default findUniqueMonthlyStat;

View File

@@ -1,31 +0,0 @@
import Elysia, { t } from "elysia";
import createMonthlyStat from "./create";
import deleteMonthlyStat from "./del";
import findManyMonthlyStat from "./findMany";
import findUniqueMonthlyStat from "./findUnique";
import updateMonthlyStat from "./updt";
const surveyResponse = new Elysia({
prefix: "/monthlystat",
tags: ["Landing Page/Indeks Kepuasan Masyarakat/Survey Response"],
})
.post("/create", createMonthlyStat, {
body: t.Object({
month: t.String(),
respondentsCount: t.Number(),
surveyId: t.String(),
}),
})
.get("/findMany", findManyMonthlyStat)
.get("/:id", findUniqueMonthlyStat)
.put("/:id", updateMonthlyStat, {
body: t.Object({
month: t.String(),
respondentsCount: t.Number(),
surveyId: t.String(),
}),
})
.delete("/del/:id", deleteMonthlyStat);
export default surveyResponse;

View File

@@ -1,34 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const updateMonthlyStat = async (context: Context) => {
const { id } = context.params as { id: string };
const body = await context.body as {
month?: string;
respondentsCount?: number;
};
try {
const updated = await prisma.monthlyStat.update({
where: { id },
data: {
month: body.month,
respondentsCount: body.respondentsCount,
},
});
return {
success: true,
message: "Data MonthlyStat berhasil diupdate",
data: updated,
};
} catch (error) {
console.error("Gagal update MonthlyStat:", error);
return {
success: false,
message: "Gagal mengupdate data statistik bulanan",
};
}
};
export default updateMonthlyStat;

View File

@@ -1,33 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const responseStatCreate = async (context: Context) => {
const body = (await context.body) as {
label: string;
count: number;
monthlyStatId: string;
};
try {
const created = await prisma.responseStat.create({
data: {
label: body.label,
count: body.count,
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Response Stat berhasil dibuat",
data: created,
};
} catch (error) {
console.error("Gagal create response stat:", error);
return {
success: false,
message: "Gagal membuat data response stat",
};
}
};
export default responseStatCreate;

View File

@@ -1,24 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const responseStatDelete = async (context: Context) => {
const { id } = context.params as { id: string };
try {
await prisma.responseStat.delete({
where: { id },
});
return {
success: true,
message: "Data response berhasil dihapus",
};
} catch (error) {
console.error("Gagal hapus Response Stat:", error);
return {
success: false,
message: "Gagal menghapus data response stat",
};
}
};
export default responseStatDelete;

View File

@@ -1,26 +0,0 @@
import prisma from "@/lib/prisma";
export const responseStatFindMany = async () => {
try {
const result = await prisma.responseStat.findMany({
include: {
MonthlyStat: true,
},
orderBy: {
id: "desc",
},
});
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Response Stat:", error);
return {
success: false,
message: "Gagal mengambil data response stat",
};
}
};
export default responseStatFindMany;

View File

@@ -1,34 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const responseStatFindUnique = async (context: Context) => {
const { id } = context.params as { id: string };
try {
const result = await prisma.responseStat.findUnique({
where: { id },
include: {
MonthlyStat: true,
},
});
if (!result) {
return {
success: false,
message: "Data response tidak ditemukan",
};
}
return {
success: true,
data: result,
};
} catch (error) {
console.error("Gagal ambil data Response Stat:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data response stat",
};
}
};
export default responseStatFindUnique;

View File

@@ -1,31 +0,0 @@
import Elysia, { t } from "elysia";
import responseStatCreate from "./create";
import responseStatDelete from "./del";
import responseStatFindMany from "./findMany";
import responseStatFindUnique from "./findUnique";
import responseStatUpdate from "./updt";
const response = new Elysia({
prefix: "/response",
tags: ["Landing Page/Indeks Kepuasan Masyarakat/Response"],
})
.post("/create", responseStatCreate, {
body: t.Object({
monthlyStatId: t.String(),
label: t.String(),
count: t.Number(),
}),
})
.get("/findMany", responseStatFindMany)
.get("/:id", responseStatFindUnique)
.put("/:id", responseStatUpdate, {
body: t.Object({
label: t.String(),
count: t.Number(),
monthlyStatId: t.String(),
}),
})
.delete("/del/:id", responseStatDelete);
export default response;

View File

@@ -1,35 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const responseStatUpdate = async (context: Context) => {
const { id } = context.params as { id: string };
const body = (await context.body) as {
label?: string;
count?: number;
monthlyStatId?: string;
};
try {
const updated = await prisma.responseStat.update({
where: { id },
data: {
label: body.label,
count: body.count,
monthlyStatId: body.monthlyStatId,
},
});
return {
success: true,
message: "Data response berhasil diperbarui",
data: updated,
};
} catch (error) {
console.error("Gagal update Response Stat:", error);
return {
success: false,
message: "Gagal memperbarui data response stat",
};
}
};
export default responseStatUpdate;

View File

@@ -1,42 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const surveyCreate = async (context: Context) => {
const body = await context.body as {
title: string;
totalRespondents: number;
averageScore: number;
};
// Validasi
if (!body.title || !body.totalRespondents || body.averageScore === undefined) {
return {
success: false,
message: "Field title, tahun, dan skor wajib diisi",
};
}
try {
const created = await prisma.survey.create({
data: {
title: body.title,
totalRespondents: body.totalRespondents,
averageScore: body.averageScore,
},
});
return {
success: true,
message: "Survey berhasil dibuat",
data: created,
};
} catch (error) {
console.error("Gagal create survey :", error);
return {
success: false,
message: "Terjadi kesalahan saat membuat survey ",
};
}
};
export default surveyCreate;

View File

@@ -1,24 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const surveyDelete = async (context: Context) => {
const { id } = context.params as { id: string };
try {
await prisma.survey.delete({
where: { id },
});
return {
success: true,
message: "Survey berhasil dihapus",
};
} catch (error) {
console.error("Gagal hapus survey:", error);
return {
success: false,
message: "Gagal menghapus survey",
};
}
};
export default surveyDelete;

View File

@@ -1,28 +0,0 @@
import prisma from "@/lib/prisma";
export const surveyFindMany = async () => {
try {
const surveys = await prisma.survey.findMany({
include: {
monthlyStats: true,
},
orderBy: {
createdAt: "desc",
},
});
return {
success: true,
data: surveys,
};
} catch (error) {
console.error("Gagal ambil data survey:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data survey",
};
}
};
export default surveyFindMany;

View File

@@ -1,34 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export const surveyFindUnique = async (context: Context) => {
const { id } = context.params as { id: string };
try {
const survey = await prisma.survey.findUnique({
where: { id },
include: {
monthlyStats: true,
},
});
if (!survey) {
return {
success: false,
message: "Survey tidak ditemukan",
};
}
return {
success: true,
data: survey,
};
} catch (error) {
console.error("Gagal ambil data survey:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data survey",
};
}
};
export default surveyFindUnique;

View File

@@ -1,30 +0,0 @@
import Elysia, { t } from "elysia";
import surveyCreate from "./create";
import surveyUpdate from "./updt";
import surveyFindMany from "./findMany";
import surveyFindUnique from "./findUnique";
import surveyDelete from "./del";
const survey = new Elysia({
prefix: "/survey",
tags: ["Landing Page/Indeks Kepuasan Masyarakat/Survey "],
})
.post("/create", surveyCreate, {
body: t.Object({
title: t.String(),
totalRespondents: t.Number(),
averageScore: t.Number(),
}),
})
.get("/findMany", surveyFindMany)
.get("/:id", surveyFindUnique)
.put("/:id", surveyUpdate, {
body: t.Object({
title: t.String(),
totalRespondents: t.Number(),
averageScore: t.Number(),
}),
})
.delete("/del/:id", surveyDelete);
export default survey;

View File

@@ -1,49 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdateSurvey = {
title?: string;
tahun?: number;
totalRespondents?: number;
averageScore?: number;
};
const surveyUpdate = async (context: Context) => {
const id = context.params?.id as string;
const body = await context.body as FormUpdateSurvey;
if (!id) {
return {
success: false,
message: "ID survey wajib diisi",
};
}
try {
const updated = await prisma.survey.update({
where: { id },
data: {
title: body.title,
totalRespondents: body.totalRespondents,
averageScore: body.averageScore,
},
include: {
monthlyStats: true,
},
});
return {
success: true,
message: "Berhasil update survey ",
data: updated,
};
} catch (error) {
console.error("Gagal update survey :", error);
return {
success: false,
message: "Gagal update survey ",
};
}
};
export default surveyUpdate;

View File

@@ -8,7 +8,6 @@ import SDGSDesa from "./sdgs-desa";
import APBDes from "./apbdes";
import PrestasiDesa from "./prestasi-desa";
import KategoriPrestasi from "./prestasi-desa/kategori-prestasi";
import IndeksKepuasanMasyarakat from "./indeks-kepuasan-masyarakat";
const LandingPage = new Elysia({
prefix: "/api/landingpage",
@@ -24,6 +23,5 @@ const LandingPage = new Elysia({
.use(APBDes)
.use(PrestasiDesa)
.use(KategoriPrestasi)
.use(IndeksKepuasanMasyarakat)
export default LandingPage

View File

@@ -1,185 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'FESTIVAL SENI BUDAYA KAB. BADUNG',
image: "/api/img/tari-3.jpg",
tanggal: "Selasa, 11 Januari 2025",
},
{
id: 2,
judul: 'LATIHAN TARI REJANG GIRI PUTRI',
image: "/api/img/tari-3.jpg",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'LATIHAN TARI REJANG GIRI PUTRI',
image: "/api/img/tari-3.jpg",
tanggal: "Kamis, 13 Januari 2025",
},
]
function Budaya() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, md: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/budaya-1.jpg"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 610 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Budaya</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
FESTIVAL SENI BUDAYA KAB. BADUNG
</Text>
<Text size="md" lineClamp={3}>
Semeton Darmasaba yang suka menikmati seni seperti baleganjur, gong kebyar, tari, dan lainnya. Nih! ada acara keren di Puspem Badung tepatnya di Balai Budaya Giri Nata Mandala yaitu Festival Seni Budaya dari tanggal 1 November 2023 s.d. 16 November 2023.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, md: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/tari-3.jpg"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Budaya</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
PELATIHAN TARI WALI
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/tari-3.jpg"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Budaya</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
LATIHAN TARI REJANG GIRI PUTRI
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt=""
fit="cover"
h={282}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Budaya</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Budaya;

View File

@@ -1,185 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'PROGRAM KETAHANAN PANGAN PEMERINTAH DESA DARMASABA TAHUN 2023',
image: "/api/img/ekonomi-sampingan-3.png",
tanggal: "Selasa, 11 Januari 2025",
},
{
id: 2,
judul: 'Sinergitas Pemkab Badung-TNI Wujudkan Kedaulatan Pangan di Subak Aban Darmasaba',
image: "/api/img/ekonomi-sampingan.png",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'ANTUSIASME WARGA DARMASABA MELAKUKAN PEMBUKAAN REKENING BANK BPD BALI ',
image: "/api/img/ekonomi-sampingan-2.png",
tanggal: "Kamis, 13 Januari 2025",
},
]
function Ekonomi() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, md: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/ekonomi-utama.png"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 660 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Ekonomi</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
PROGRAM KETAHANAN PANGAN PEMERINTAH DESA DARMASABA TAHUN 2023
</Text>
<Text size="md" lineClamp={2}>
Pemerintah Desa Darmasaba melalui kegiatan ketahanan pangan ini menjalankan dua kategori yaitu pertanian dan peternakan untuk kategori pertanian telah membuahkan hasil panen pertama pada hari Kamis, 24 Agustus 2023 melakukan panen bawang merah di lokasi ketahanan pangan Br. Taman, Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung. Adapun varietas bawang yang dipanen adalah Bawang Bali Karet (Batu Ijo) pada lahan seluas kurang lebih 7 are.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, md: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/ekonomi-sampingan.png"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Ekonomi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Sinergitas Pemkab Badung-TNI Wujudkan Kedaulatan Pangan di Subak Aban Darmasaba
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/ekonomi-sampingan-2.png"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Ekonomi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={1}>
ANTUSIASME WARGA DARMASABA MELAKUKAN PEMBUKAAN REKENING BANK BPD BALI
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Ekonomi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Ekonomi;

View File

@@ -1,187 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'PEMBANGUNAN GOT JALAN LINGKUNGAN BR. TAMAN',
image: "/api/img/pembangunan-1.jpg",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 2,
judul: 'PEMBANGUNAN BALE GAMBELAN SETRA AGENG DESA ADAT TEGAL',
image: "/api/img/pembangunan-3.jpg",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'PEMBANGUNAN TROTOARISASI JALAN LINGKUNGAN BR. GULINGAN',
image: "/api/img/pembangunan-1.jpg",
tanggal: "Selasa, 11 Januari 2025",
},
]
function Pembangunan() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, xl: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/pembangunan-1.jpg"
alt="PEMBANGUNAN TROTOARISASI JALAN LINGKUNGAN BR. GULINGAN"
radius="md"
fit="cover"
h={{ base: 450, md: 615 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pembangunan</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
PEMBANGUNAN TROTOARISASI JALAN LINGKUNGAN BR. GULINGAN
</Text>
<Text size="md" lineClamp={3}>
Sudah tertata rapi dan bersih niki Semeton Darmasaba! Sekarang pejalan kaki di seputaran jalan lingkungan Br. Gulingan bisa aman dan nyaman nie!!!
Pemdes Darmasaba dalam rangka memenuhi kebutuhan masyarakat khususnya warga Br. Gulingan yang memerlukan trotoarisasi guna memperlancar saluran air dan memberikan kenyamanan ketika berjalan, kini sudah menyelesaikan pembangunan trotoarisasi jalan lingkungan Br. Gulingan. Pembangunan tersebut menggunakan APBDes Darmasaba T. A. 2023 dengan realisasi sebesar Rp357.011.299,00 dari pagu yang disiapkan sebesar Rp439.066.327,00. Pembangunan trotoar tersebut melibatkan pekerja lokal dan dibantu oleh warga Br. Gulingan.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, xl: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/pembangunan-2.jpg"
alt="Prestasi Voli"
fit="cover"
h={300}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pembangunan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
PEMBANGUNAN GOT JALAN LINGKUNGAN BR. TAMAN
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/pembangunan-3.jpg"
alt="Prestasi Voli"
fit="cover"
h={250}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pembangunan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
PEMBANGUNAN BALE GAMBELAN SETRA AGENG DESA ADAT TEGAL
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={200}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pembangunan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Pembangunan;

View File

@@ -1,185 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'PEMBERIAN HASIL PANEN JAGUNG PROGRAM KETAHANAN PANGAN DESA KEPADA ANAK YATIM, PIATU DAN YATIM-PIATU DESA DARMASABA',
image: "/api/img/berita-pemerintahan.jpg",
tanggal: "Selasa, 11 Januari 2025",
},
{
id: 2,
judul: 'Tim Voli Darmasaba Raih Juara Turnamen Desa',
image: "/api/img/pemrintahan-5.png",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'PEMBERIAN HASIL PANEN JAGUNG PROGRAM KETAHANAN PANGAN DESA KEPADA ANAK YATIM, PIATU DAN YATIM-PIATU DESA DARMASABA',
image: "/api/img/berita-pemerintahan.jpg",
tanggal: "Kamis, 13 Januari 2025",
},
]
function Pemerintahan() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, xl: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/berita-pemerintahan.jpg"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 615 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pemerintahan</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
PEMBERIAN HASIL PANEN JAGUNG PROGRAM KETAHANAN PANGAN DESA KEPADA ANAK YATIM, PIATU DAN YATIM-PIATU DESA DARMASABA
</Text>
<Text size="md" lineClamp={3}>
Pada hari Selasa, 19 November 2024, Desa Darmasaba memberikan hasil panen jagung sebagai bagian dari kegiatan Ketahanan Pangan Desa kepada anak-anak yatim, piatu, dan yatim-piatu yang berusia 0-18 Tahun. Hasil panen ini diserahkan langsung oleh I. B. Surya Prabhawa Manuaba, S.H., M.H., NL.P selaku Perbekel Darmasaba, didampingi Ketua BPD dan LPM Desa Darmasaba, serta seluruh perangkat dan staf desa. Melalui kegiatan ini, kami berharap dapat memberikan manfaat langsung bagi mereka yang membutuhkan, serta memperkuat rasa kebersamaan dan kepedulian dalam membangun ketahanan pangan di Desa Darmasaba.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, xl: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/beritapemerintahan-2.jpeg"
alt="Prestasi Voli"
fit="cover"
h={300}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pemerintahan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Evaluasi dan Verifikasi APBDes Darmasaba T.A. 2025
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/pemerintahan-3.jpg"
alt="Prestasi Voli"
fit="cover"
h={250}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pemerintahan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
JAKSA GARDA DESA (JAGA DESA) T.A. 2025
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={200}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pemerintahan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Pemerintahan;

View File

@@ -1,188 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'Darmasaba Luncurkan Aplikasi Smart Village untuk Pelayanan Publik',
image: "/api/img/berita-1.png",
tanggal: "Selasa, 11 Januari 2025",
kategori: "Teknologi"
},
{
id: 2,
judul: 'Tim Voli Darmasaba Raih Juara Turnamen Desa',
image: "/api/img/prestasi-voli.jpeg",
tanggal: "Kamis, 13 Januari 2025",
kategori: "Sosial"
},
{
id: 3,
judul: 'DARMASABA DIGITAL PROJECT IS COMING!',
image: "/api/img/teknologi-1.png",
tanggal: "Kamis, 13 Januari 2025",
kategori: "Teknologi"
},
]
function Semua() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, xl: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/berita-1.png"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 610 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Teknologi</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
Darmasaba Luncurkan Aplikasi Smart Village untuk Pelayanan Publik
</Text>
<Text size="md" lineClamp={3}>
Desa Darmasaba mengambil langkah maju dalam transformasi digital dengan meluncurkan aplikasi Smart Village yang memudahkan warga dalam mengakses layanan publik secara online.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, xl: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/pembangunan-2.jpg"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Pembangunan</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Darmasaba Luncurkan Aplikasi Smart Village untuk Pelayanan Publik
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/prestasi-voli.jpeg"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Sosial</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Tim Voli Darmasaba Raih Juara Turnamen Desa
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, xl: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={{ base: 180, xl: 250 }}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">{v.kategori}</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Semua;

View File

@@ -1,204 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'AKSI BERSIH DESA MEMPERINGATI HARI PEDULI SAMPAH NASIONAL 2025',
image: "/api/img/sosial-4.png",
tanggal: "Selasa, 11 Januari 2025",
},
{
id: 2,
judul: 'Sosialisasi Pengelolaan Sampah di SD No. 3 Darmasaba dalam Kolaborasi Mahasiswa KKN-PMM I Universitas Warmadewa di Desa Darmasaba',
image: "/api/img/sosial-2.jpeg",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'Tim Voli Darmasaba Raih Juara Turnamen Desa',
image: "/api/img/prestasi-voli.jpeg",
tanggal: "Kamis, 13 Januari 2025",
},
]
function Sosial() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, md: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/sosial-1.jpeg"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 635 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Sosial</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
AKSI BERSIH DESA MEMPERINGATI HARI PEDULI SAMPAH NASIONAL 2025
</Text>
<Text size="md" lineClamp={3}>
AKSI BERSIH DESA MEMPERINGATI HARI PEDULI SAMPAH NASIONAL 2025
Dalam semangat Hari Peduli Sampah Nasional (HPSN) 2025, Aksi Bersih Desa sukses digelar di Lombok Utara serta beberapa wilayah lain, termasuk Kabupaten Badung yang berpusat di TPS 3R Pudak Mesari, Desa Darmasaba. ?
Kegiatan ini melibatkan berbagai pihak seperti Pusdal LH BN - KLH, DKLH Provinsi Bali, DLHK Kab. Badung, Pemdes Darmasaba, BPD, LPM, TPS 3R Pudak Mesari, Lembaga Kemasyarakatan Desa, serta masyarakat Darmasaba.
Selain aksi bersih, Desa Darmasaba juga meluncurkan Pemuda Peduli Lingkungan sebagai garda terdepan dalam gerakan sadar lingkungan!
Mengapa ini penting?
- Pengelolaan sampah harus dimulai dari sumbernya melalui pemilahan, pemanfaatan, hingga penyelesaian residu.
- Inovasi &quot;Galah Melah&quot; & &quot;BARES&quot; menjadi contoh nyata dalam menciptakan lingkungan yang bersih & berkelanjutan.
- Darmasaba terpilih sebagai 1 dari 8 desa se-Indonesia untuk peringatan HPSN 2025, sebuah kebanggaan sekaligus tanggung jawab bersama!
&quot;Kami berharap aksi ini bukan sekadar seremoni, tetapi menjadi budaya masyarakat dalam memilah dan mengelola sampah dari sumbernya.&quot; - Ibu Cokorda Istri Muter Handayani, Plt. Kabid Wilayah Bali, Pusat Pengendalian LH Bali & Nusra.
&quot;Ke depan, kami menargetkan setiap desa di Badung memiliki komunitas Pemuda Peduli Lingkungan agar pengelolaan sampah lebih optimal dan berkelanjutan.&quot; - Bapak Ida Bagus Gede Arjana, Plt. Kadis DLHK Kab. Badung.
&quot;Darmasaba menjadi contoh desa aktif dalam pengelolaan sampah. Kami berharap inovasi ini menginspirasi desa lain.&quot; - Ida Bagus Surya Prabhawa Manuaba, Perbekel Darmasaba.
Mari bersama wujudkan lingkungan yang lebih bersih, sehat, dan lestari!
Jaga Bumi, Pilah Sampah dari Sekarang!
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, md: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/sosial-2.jpeg"
alt="Darmasaba Smart Village"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Sosial</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Sosialisasi Pengelolaan Sampah di SD No. 3 Darmasaba dalam Kolaborasi Mahasiswa KKN-PMM I Universitas Warmadewa di Desa Darmasaba
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/prestasi-voli.jpeg"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Sosial</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
Tim Voli Darmasaba Raih Juara Turnamen Desa
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Sosial</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Sosial;

View File

@@ -1,186 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Container, Divider, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import React from 'react';
const dataBeritaTerbaru = [
{
id: 1,
judul: 'Darmasaba Luncurkan Aplikasi Smart Village untuk Pelayanan Publik',
image: "/api/img/teknologi-3.png",
tanggal: "Selasa, 11 Januari 2025",
},
{
id: 2,
judul: 'DARMASABA DIGITAL PROJECT IS COMING!',
image: "/api/img/teknologi-1.png",
tanggal: "Kamis, 13 Januari 2025",
},
{
id: 3,
judul: 'DARMASABA MENUJU DESA DIGITAL',
image: "/api/img/teknologi-2.png",
tanggal: "Kamis, 13 Januari 2025",
},
]
function Teknologi() {
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Grid gutter={{ base: "md", md: "xl" }} pb={70}>
{/* Berita Utama */}
<GridCol span={{ base: 12, md: 8 }}>
<Paper p="md" shadow="sm" radius="md">
<Stack gap="md">
<Box>
<Image
src="/api/img/berita-1.png"
alt="Darmasaba Smart Village"
radius="md"
fit="cover"
h={{ base: 450, md: 660 }}
/>
</Box>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Teknologi</Text>
</Paper>
</Group>
<Text fz={{ base: "xl", md: "2xl" }} fw="bold" lineClamp={2}>
Darmasaba Luncurkan Aplikasi Smart Village untuk Pelayanan Publik
</Text>
<Text size="md" lineClamp={3}>
Desa Darmasaba mengambil langkah maju dalam transformasi digital dengan meluncurkan aplikasi Smart Village yang memudahkan warga dalam mengakses layanan publik secara online.
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</GridCol>
{/* Berita Sampingan */}
<GridCol span={{ base: 12, md: 4 }}>
<Stack gap="md">
{/* Berita Sampingan 1 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/teknologi-1.png"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Teknologi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
DARMASABA DIGITAL PROJECT IS COMING! Desa Darmasaba nggak pernah berhenti berinovasi! ?
Kali ini, kami siap menghadirkan gebrakan baru yang akan mempermudah sistem kerja di lingkungan
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
{/* Berita Sampingan 2 */}
<Paper p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src="/api/img/teknologi-2.png"
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Teknologi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={1}>
DARMASABA MENUJU DESA DIGITAL
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
</GridCol>
</Grid>
<Box pb={30}>
<Text fz={"h1"} fw={"bold"}>Berita Terbaru</Text>
<Divider color={colors["blue-button"]} />
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataBeritaTerbaru.map((v, k) => {
return (
<Paper key={k} p="md" shadow="sm" radius="md">
<Stack gap="sm">
<Image
radius="md"
src={v.image}
alt="Prestasi Voli"
fit="cover"
h={180}
/>
<Group>
<Paper px={10} py={5} radius="xl" bg={colors['BG-trans']}>
<Text size="sm">Teknologi</Text>
</Paper>
</Group>
<Text fz="lg" fw="bold" lineClamp={2}>
{v.judul}
</Text>
<Group>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Box py={"xl"}>
<Center>
<Pagination total={10} />
</Center>
</Box>
</Container>
</Box>
);
}
export default Teknologi;

View File

@@ -0,0 +1,168 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import {
Badge,
Box,
Button,
Card,
Center,
Container,
Divider,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
export default function Content({ kategori }: { kategori: string }) {
const router = useTransitionRouter();
const [page, setPage] = useState(1);
const state = useProxy(stateDashboardBerita.berita);
const featuredState = useProxy(stateDashboardBerita.berita.findFirst);
const featured = featuredState.data;
const paginatedNews = state.findMany.data || [];
const totalPages = state.findMany.totalPages || 1;
// Load data
useEffect(() => {
stateDashboardBerita.berita.findFirst.load(kategori);
}, [kategori]);
useEffect(() => {
state.findMany.load(page, 3, '', kategori);
}, [page, kategori]);
return (
<Box py={20}>
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Berita Utama === */}
{featuredState.loading ? (
<Center><Skeleton h={400} /></Center>
) : featured ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featured.image?.link}
alt={featured.judul || 'Berita Utama'}
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} p="xl">
<Stack h="100%" justify="space-between">
<div>
<Badge color="blue" variant="light" mb="md">
{featured.kategoriBerita?.name || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text color="dimmed" lineClamp={3} mb="md">{featured.deskripsi}</Text>
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Group>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)}
>
Baca Selengkapnya
</Button>
</Group>
</Stack>
</GridCol>
</Grid>
</Paper>
</Box>
) : null}
{/* === Daftar Berita === */}
<Box mt={50}>
<Title order={2} mb="md">Daftar Berita</Title>
<Divider mb="xl" />
{state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Belum ada berita di kategori &quot;{kategori}&quot;.</Text>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
<Card
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/desa/berita/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<Card.Section>
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" />
</Card.Section>
<Badge color="blue" variant="light" mt="md">
{item.kategoriBerita?.name || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" color="dimmed" lineClamp={3} mt="xs">{item.deskripsi}</Text>
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" color="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
</Group>
</Card>
))}
</SimpleGrid>
)}
{/* Pagination */}
<Center mt="xl">
<Pagination
total={totalPages}
value={page}
onChange={(newPage) => setPage(newPage)}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
</Box>
</Container>
</Box>
);
}

View File

@@ -0,0 +1,78 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import { Box, Center, Container, Image, Skeleton, Stack, Text } from '@mantine/core';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../layanan/_com/BackButto';
function Page() {
const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const state = useProxy(stateDashboardBerita.berita)
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadData = async () => {
if (!id) return;
try {
setLoading(true);
await state.findUnique.load(id);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
}
loadData()
}, [id])
if (loading) {
return (
<Center>
<Skeleton height={500} />
</Center>
);
}
if (!state.findUnique.data) {
return (
<Center>
<Text>Data tidak ditemukan</Text>
</Center>
);
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
<Box px={{ base: "md", md: 100 }}><BackButton /></Box>
<Container w={{ base: "100%", md: "50%" }} >
<Box pb={20}>
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
{state.findUnique.data?.judul}
</Text>
<Text
ta={"center"}
fw={"bold"}
fz={"1.5rem"}
>
Informasi dan Pelayanan Administrasi Digital
</Text>
</Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} />
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,13 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./Content";
export default async function Page({ params }: { params: Promise<{ kategori: string }> }) {
const { kategori } = await params;
return (
<Suspense fallback={<div>Loading...</div>}>
<Content kategori={kategori} />
</Suspense>
);
}

View File

@@ -0,0 +1,181 @@
'use client'
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import BackButton from '../../layanan/_com/BackButto';
type HeaderSearchProps = {
placeholder?: string;
searchIcon?: React.ReactNode;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
children?: React.ReactNode;
};
function LayoutTabsBerita({
children,
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />
}: HeaderSearchProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// Get active tab from URL path
const activeTab = pathname.split('/').pop() || 'semua';
// Get initial search value from URL
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Update active tab state when pathname changes
const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
// Clean up timeouts on unmount
useEffect(() => {
return () => {
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
};
}, [searchTimeout]);
// Handle search input change with debounce
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Clear previous timeout
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
// Set new timeout
const newTimeout = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) {
router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
}
}, 500); // 500ms debounce delay
setSearchTimeout(newTimeout);
};
const tabs = [
{
label: "Semua",
value: "semua",
href: "/darmasaba/desa/berita/semua"
},
{
label: "Budaya",
value: "budaya",
href: "/darmasaba/desa/berita/budaya"
},
{
label: "Pemerintahan",
value: "pemerintahan",
href: "/darmasaba/desa/berita/pemerintahan"
},
{
label: "Ekonomi",
value: "ekonomi",
href: "/darmasaba/desa/berita/ekonomi"
},
{
label: "Pembangunan",
value: "pembangunan",
href: "/darmasaba/desa/berita/pembangunan"
},
{
label: "Sosial",
value: "sosial",
href: "/darmasaba/desa/berita/sosial"
},
{
label: "Teknologi",
value: "teknologi",
href: "/darmasaba/desa/berita/teknologi"
},
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = tabs.find(t => t.value === value);
if (tab) {
const params = new URLSearchParams(searchParams.toString());
router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
}
};
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Portal Berita Darmasaba
</Text>
<Text ta="center" px="md">
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
</Container>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTabState}
onChange={handleTabChange}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</GridCol>
</Grid>
</Box>
{children}
</Tabs>
</Stack>
);
}
export default LayoutTabsBerita;

View File

@@ -0,0 +1,12 @@
// app/desa/berita/BeritaLayoutClient.tsx
'use client'
import dynamic from 'next/dynamic';
const LayoutTabsBerita = dynamic(
() => import('./_lib/layoutTabs'),
{ ssr: false }
);
export default function BeritaLayoutClient({ children }: { children: React.ReactNode }) {
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
}

View File

@@ -1,102 +0,0 @@
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsPanel, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import BackButton from '../layanan/_com/BackButto';
import Budaya from './(tabs)/budaya';
import Ekonomi from './(tabs)/ekonomi';
import Pemerintahan from './(tabs)/pemerintahan';
import Semua from './(tabs)/semua';
import Sosial from './(tabs)/sosial';
import Teknologi from './(tabs)/teknologi';
import Pembangunan from './(tabs)/pembangunan';
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Portal Berita Darmasaba
</Text>
<Text ta="center" px="md">
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
</Container>
{/* Tabs Menu */}
<Tabs color={colors['blue-button']} variant="pills" defaultValue="semua">
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']} >
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
<TabsTab value="semua">
Semua
</TabsTab>
<TabsTab value="pemerintahan">
Pemerintahan
</TabsTab>
<TabsTab value="pembangunan" >
Pembangunan
</TabsTab>
<TabsTab value="ekonomi" >
Ekonomi
</TabsTab>
<TabsTab value="sosial" >
Sosial
</TabsTab>
<TabsTab value="budaya" >
Budaya
</TabsTab>
<TabsTab value="teknologi" >
Teknologi
</TabsTab>
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
w={{ base: "100%", md: "100%" }}
radius="lg"
placeholder="Cari Berita"
leftSection={<IconSearch size={18} />}
/>
</GridCol>
</Grid>
</Box>
<TabsPanel value='semua'>
<Semua />
</TabsPanel>
<TabsPanel value='pemerintahan'>
<Pemerintahan />
</TabsPanel>
<TabsPanel value="pembangunan" >
<Pembangunan />
</TabsPanel>
<TabsPanel value="ekonomi" >
<Ekonomi />
</TabsPanel>
<TabsPanel value="sosial" >
<Sosial />
</TabsPanel>
<TabsPanel value="budaya" >
<Budaya />
</TabsPanel>
<TabsPanel value="teknologi" >
<Teknologi />
</TabsPanel>
</Tabs>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, Skeleton, Stack, Tabs, TabsList, TabsPanel, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import BackButton from '../layanan/_com/BackButto';
import Budaya from './budaya/page';
import Ekonomi from './ekonomi/page';
import Pemerintahan from './pemerintahan/page';
import Semua from './semua/page';
import Sosial from './sosial/page';
import Teknologi from './teknologi/page';
import Pembangunan from './pembangunan/page';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
function Page() {
const [selectedKategori, setSelectedKategori] = useState<string>('Semua');
const [loading, setLoading] = useState(false);
const state = useProxy(stateDashboardBerita);
useEffect(() => {
state.kategoriBerita.findMany.load()
const loadData = async () => {
try {
setLoading(true);
await stateDashboardBerita.berita.findMany.load(1, 100);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
loadData();
}, []);
const data = state.berita.findMany.data || [];
const categories = [...new Set(
data
.filter(item => item.kategoriBerita?.name)
.map(item => item.kategoriBerita?.name)
)];
const filteredData = selectedKategori === 'Semua'
? data
: data.filter(item => item.kategoriBerita?.name === selectedKategori);
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Portal Berita Darmasaba
</Text>
<Text ta="center" px="md">
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
</Container>
{/* Tabs Menu */}
<Tabs color={colors['blue-button']} variant="pills" defaultValue="semua">
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']} >
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{categories.map((kategori) => (
<TabsTab key={kategori} value={kategori || ''}>
{kategori}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
{loading ? (
<Skeleton height={42} />
) : filteredData.length > 0 ? (
<TextInput
w={{ base: "100%", md: "100%" }}
radius="lg"
placeholder="Cari Berita"
leftSection={<IconSearch size={18} />}
/>
) : (
<Text>Tidak ada data</Text>
)}
</GridCol>
</Grid>
</Box>
<TabsPanel value='semua'>
<Semua />
</TabsPanel>
<TabsPanel value='pemerintahan'>
<Pemerintahan />
</TabsPanel>
<TabsPanel value="pembangunan" >
<Pembangunan />
</TabsPanel>
<TabsPanel value="ekonomi" >
<Ekonomi />
</TabsPanel>
<TabsPanel value="sosial" >
<Sosial />
</TabsPanel>
<TabsPanel value="budaya" >
<Budaya />
</TabsPanel>
<TabsPanel value="teknologi" >
<Teknologi />
</TabsPanel>
</Tabs>
</Stack>
);
}
export default Page;

Some files were not shown because too many files have changed in this diff Show More