Compare commits

...

4 Commits

Author SHA1 Message Date
3654629bde Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Akan Dikerjakan:
* API ProfilePPID
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 17:15:07 +08:00
02738104b5 Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, dan delete berita

Yang Akan Dikerjakan:
* API Di Menu Desa : Edit Berita
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 14:26:10 +08:00
92de697ae0 Nico-25 Mei 2025:
Membuat create berita dan gambar
Membuat fungsi tombol di mana bisa menghapus konten sesuai idnya
2025-05-25 11:33:50 +08:00
cf6a5422ec Bagian Berita Di Admin Sudah Bisa Upload Di Bun Dev atau Bun Start 2025-05-23 16:30:46 +08:00
35 changed files with 1770 additions and 154 deletions

View File

@@ -242,17 +242,18 @@ model Berita {
id String @id @default(cuid())
judul String
deskripsi String
image String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
KatagoryBerita KatagoryBerita? @relation(fields: [katagoryBeritaId], references: [id])
katagoryBeritaId String?
kategoriBerita KategoriBerita? @relation(fields: [kategoriBeritaId], references: [id])
kategoriBeritaId String?
}
model KatagoryBerita {
model KategoriBerita {
id String @id @default(cuid())
name String @unique
beritas Berita[]
@@ -596,4 +597,5 @@ model FileStorage {
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
}

View File

@@ -1,6 +1,6 @@
import prisma from "@/lib/prisma";
import categoryPengumuman from "./data/category-pengumuman.json";
import katagoryBerita from "./data/katagory-berita.json";
import kategoriBerita from "./data/kategori-berita.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
@@ -49,8 +49,8 @@ import { v4 as uuid } from "uuid";
console.log("potensi success ...");
for (const k of katagoryBerita) {
await prisma.katagoryBerita.upsert({
for (const k of kategoriBerita) {
await prisma.kategoriBerita.upsert({
where: {
name: k.name,
},
@@ -63,7 +63,7 @@ import { v4 as uuid } from "uuid";
});
}
console.log("katagory berita success ...");
console.log("kategori berita success ...");
for (const c of categoryPengumuman) {
await prisma.categoryPengumuman.upsert({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -0,0 +1,36 @@
// components/modal/ModalKonfirmasiHapus.tsx
import colors from "@/con/colors"
import { Modal, Text, Button, Flex } from "@mantine/core"
interface ModalKonfirmasiHapusProps {
opened: boolean
loading?: boolean
onClose: () => void
onConfirm: () => void
text: string
}
export function ModalKonfirmasiHapus({
opened,
loading = false,
onClose,
onConfirm,
text,
}: ModalKonfirmasiHapusProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title="Konfirmasi Hapus"
centered
>
<Text mb="md">{text}</Text>
<Flex justify="flex-end" gap="sm">
<Button style={{color: "white"}} bg={colors['blue-button']} variant="default" onClick={onClose}>Batal</Button>
<Button color="red" onClick={onConfirm} loading={loading}>
Yakin Hapus
</Button>
</Flex>
</Modal>
)
}

View File

@@ -1,23 +1,32 @@
/* 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";
// 1. Schema validasi dengan Zod
const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
image: z.string().url().min(3, "Image minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
katagoryBeritaId: z.string().nonempty(),
kategoriBeritaId: z.string().nonempty(),
imageId: z.string().nonempty(),
});
// 2. Default value form berita (hindari uncontrolled input)
const defaultForm = {
judul: "",
deskripsi: "",
imageId: "",
content: "",
kategoriBeritaId: "",
};
// 3. Kategori proxy
const category = proxy({
findMany: {
data: null as
| null
| Prisma.KatagoryBeritaGetPayload<{ omit: { isActive: true } }>[],
data: [] as Prisma.KategoriBeritaGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.berita.category["find-many"].get();
if (res.status === 200) {
@@ -27,23 +36,12 @@ const category = proxy({
},
});
type BeritaForm = Prisma.BeritaGetPayload<{
select: {
judul: true;
deskripsi: true;
image: true;
content: true;
katagoryBeritaId: true;
};
}>;
// 4. Berita proxy
const berita = proxy({
create: {
form: {} as BeritaForm,
form: { ...defaultForm }, // ✅ ini kunci fix-nya
loading: false,
async create() {
berita.create.form.image =
"https://www.shutterstock.com/image-vector/lower-news-live-streaming-breaking-600nw-2535984111.jpg";
const cek = templateForm.safeParse(berita.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
@@ -51,6 +49,7 @@ const berita = proxy({
.join("\n")}] required`;
return toast.error(err);
}
try {
berita.create.loading = true;
const res = await ApiFetch.api.desa.berita["create"].post(
@@ -58,7 +57,7 @@ const berita = proxy({
);
if (res.status === 200) {
berita.findMany.load();
return toast.success("succes create");
return toast.success("success create");
}
return toast.error("failed create");
@@ -68,23 +67,166 @@ const berita = proxy({
berita.create.loading = false;
}
},
resetForm() {
berita.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.BeritaGetPayload<{ omit: { isActive: true } }>[]
| Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.berita["find-many"].get();
if (res.status === 200) {
berita.findMany.data = (res.data?.data as any) ?? [];
berita.findMany.data = (res.data?.data ) ?? [];
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
berita.delete.loading = true;
const response = await fetch(`/api/desa/berita/delete/${id}`, {
method: 'DELETE',
headers: {
'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
} else {
toast.error(result?.message || "Gagal menghapus berita");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus berita");
} finally {
berita.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/berita/${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 = {
judul: data.judul,
deskripsi: data.deskripsi,
content: data.content,
kategoriBeritaId: data.kategoriBeritaId || "",
imageId: data.imageId || "",
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(berita.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
berita.edit.loading = true;
const response = await fetch(`/api/desa/berita/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
content: this.form.content,
kategoriBeritaId: this.form.kategoriBeritaId || null,
imageId: this.form.imageId,
}),
});
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 berita");
await berita.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update berita");
}
} catch (error) {
console.error("Error updating berita:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update berita");
return false;
} finally {
berita.edit.loading = false;
}
},
reset() {
berita.edit.id = "";
berita.edit.form = { ...defaultForm };
},
},
});
// 5. State global
const stateDashboardBerita = proxy({
category,
berita,
});
export default stateDashboardBerita;
export default stateDashboardBerita;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { RichTextEditor, Link } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
@@ -8,28 +9,77 @@ import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Button, Stack } from '@mantine/core';
import { useEffect, useState } from 'react';
const content =
'<h2 style="text-align: center;">Welcome to Mantine rich text editor</h2><p><code>RichTextEditor</code> component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. <code>RichTextEditor</code> is based on <a href="https://tiptap.dev/" rel="noopener noreferrer" target="_blank">Tiptap.dev</a> and supports all of its features:</p><ul><li>General text formatting: <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <s>strike-through</s> </li><li>Headings (h1-h6)</li><li>Sub and super scripts (<sup>&lt;sup /&gt;</sup> and <sub>&lt;sub /&gt;</sub> tags)</li><li>Ordered and bullet lists</li><li>Text align&nbsp;</li><li>And all <a href="https://tiptap.dev/extensions" target="_blank" rel="noopener noreferrer">other extensions</a></li></ul>';
// const content =
// '<h2 style="text-align: center;">Welcome to Mantine rich text editor</h2><p><code>RichTextEditor</code> component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. <code>RichTextEditor</code> is based on <a href="https://tiptap.dev/" rel="noopener noreferrer" target="_blank">Tiptap.dev</a> and supports all of its features:</p><ul><li>General text formatting: <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <s>strike-through</s> </li><li>Headings (h1-h6)</li><li>Sub and super scripts (<sup>&lt;sup /&gt;</sup> and <sub>&lt;sub /&gt;</sub> tags)</li><li>Ordered and bullet lists</li><li>Text align&nbsp;</li><li>And all <a href="https://tiptap.dev/extensions" target="_blank" rel="noopener noreferrer">other extensions</a></li></ul>';
export function BeritaEditor({ onSubmit }: { onSubmit: (val: string) => void }) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content,
immediatelyRender: false
});
if (!editor) {
return null;
}
export function BeritaEditor({
onEditorReady,
showSubmit = true,
onSubmit,
initialContent = '',
onUpdate,
}: {
onEditorReady?: (editor: any | null) => void;
onSubmit?: (val: string) => void;
showSubmit?: boolean;
initialContent?: string;
onUpdate?: (content: string) => void;
}) {
const [mounted, setMounted] = useState(false);
const [isReady, setIsReady] = useState(false);
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: initialContent || '<p></p>',
onUpdate: ({ editor }) => {
if (onUpdate) {
onUpdate(editor.getHTML());
}
},
editorProps: {
attributes: {
class: 'prose max-w-none',
},
},
onSelectionUpdate: () => {
if (!isReady && editor) {
setIsReady(true);
onEditorReady?.(editor);
}
},
immediatelyRender: false
});
useEffect(() => {
if (editor) {
// Set initial content when component mounts
editor.commands.setContent(initialContent || '<p></p>');
// Mark as mounted and notify parent
if (!mounted) {
setMounted(true);
onEditorReady?.(editor);
}
}
return () => {
if (editor) {
editor.destroy();
}
};
}, [editor, initialContent, mounted, onEditorReady]);
if (!editor) return <div>Loading editor...</div>;
return (
@@ -82,10 +132,12 @@ export function BeritaEditor({ onSubmit }: { onSubmit: (val: string) => void })
<RichTextEditor.Content />
</RichTextEditor>
<Button onClick={() => {
if (!editor) return
onSubmit(editor?.getHTML())
}}>Submit</Button>
{showSubmit && (
<Button onClick={() => {
if (!editor) return
onSubmit?.(editor?.getHTML())
}}>Submit</Button>
)}
</Stack>
);
}

View File

@@ -0,0 +1,265 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import {
Box,
Button,
Center,
Image,
Paper,
Select,
Skeleton,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { IconImageInPicture } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { useProxy } from "valtio/utils";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import stateDashboardBerita from "../../../../_state/desa/berita";
import { Prisma } from "@prisma/client";
import { useShallowEffect } from "@mantine/hooks";
import { BeritaEditor } from "../../_com/BeritaEditor";
function BeritaEdit() {
const beritaState = useProxy(stateDashboardBerita);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [editorInstance, setEditorInstance] = useState<any>(null);
const [isEditorReady, setIsEditorReady] = useState(false);
const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || '',
deskripsi: beritaState.berita.edit.form.deskripsi || '',
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
content: beritaState.berita.edit.form.content || '',
imageId: beritaState.berita.edit.form.imageId || ''
});
// Load berita by id saat pertama kali
useEffect(() => {
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
kategoriBeritaId: data.kategoriBeritaId || '',
content: data.content || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
// Handle editor ready
const handleEditorReady = (editor: any) => {
setEditorInstance(editor);
setIsEditorReady(true);
// Set initial content if exists
if (formData.content) {
editor.commands.setContent(formData.content);
}
};
const handleSubmit = async () => {
if (!isEditorReady || !editorInstance) {
return toast.error("Editor belum siap");
}
try {
const htmlContent = editorInstance.getHTML();
if (!htmlContent || htmlContent === "<p></p>") {
return toast.warn("Konten tidak boleh kosong");
}
// Update form data with editor content
const updatedFormData = {
...formData,
content: htmlContent
};
// Update global state with form data
beritaState.berita.edit.form = {
judul: updatedFormData.judul,
deskripsi: updatedFormData.deskripsi,
content: updatedFormData.content,
kategoriBeritaId: updatedFormData.kategoriBeritaId || '',
imageId: beritaState.berita.edit.form.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
beritaState.berita.edit.form.imageId = uploaded.id;
}
await beritaState.berita.edit.update();
toast.success("Berita berhasil diperbarui!");
router.push("/admin/desa/berita");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
}
};
return (
<Box py={10}>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<TextInput
value={formData.judul}
onChange={(e) => setFormData({...formData, judul: e.target.value})}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<TextInput
value={formData.deskripsi}
onChange={(e) => setFormData({...formData, deskripsi: e.target.value})}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<BeritaEditor
initialContent={formData.content}
onEditorReady={handleEditorReady}
showSubmit={false}
onUpdate={(content) => setFormData({...formData, content})}
/>
</Box>
<Button onClick={handleSubmit}>Simpan Perubahan</Button>
</Stack>
</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"
/>
);
}
export default BeritaEdit;

View File

@@ -1,101 +1,331 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { Box, Center, Group, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { ActionIcon, Box, Button, Center, FileInput, Flex, Image, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { IconImageInPicture } from '@tabler/icons-react';
import { IconEdit, IconImageInPicture, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../_state/desa/berita';
import { BeritaEditor } from './_com/BeritaEditor';
import colors from '@/con/colors';
function Page() {
return (
<Box>
<Title order={3}>Berita</Title>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<BeritaCreate />
<BeritaList />
</SimpleGrid>
<BeritaCreate />
<BeritaList />
</Box>
);
}
function BeritaCreate() {
const beritaState = useProxy(stateDashboardBerita)
const beritaState = useProxy(stateDashboardBerita);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [editorInstance, setEditorInstance] = useState<any>(null);
const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = {
judul: "",
deskripsi: "",
kategoriBeritaId: "",
imageId: "",
content: "",
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
if (editorInstance) {
editorInstance.commands.setContent(""); // Kosongkan editor
}
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
if (!editorInstance) return toast.error("Editor belum siap");
const htmlContent = editorInstance.getHTML();
if (!htmlContent || htmlContent === "<p></p>") return toast.warn("Konten tidak boleh kosong");
beritaState.berita.create.form.content = htmlContent;
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Simpan ID gambar ke form
beritaState.berita.create.form.imageId = uploaded.id;
// Submit data berita
await beritaState.berita.create.create();
toast.success("Berita berhasil disimpan!");
// Reset form setelah submit
resetForm();
};
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<SelectCategory onChange={(val) => {
beritaState.berita.create.form.katagoryBeritaId = val.id
}} />
<TextInput onChange={(val) => {
beritaState.berita.create.form.judul = val.target.value
}} label={"Judul"} placeholder='masukkan judul' />
<TextInput onChange={(val) => {
beritaState.berita.create.form.deskripsi = val.target.value
}} label={"Deskripsi"} placeholder='masukkan deskripsi' />
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
<BeritaEditor onSubmit={(val) => {
<TextInput
value={beritaState.berita.create.form.judul}
onChange={(val) => {
beritaState.berita.create.form.judul = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<SelectCategory
onChange={(val) => {
beritaState.berita.create.form.kategoriBeritaId = val.id;
}}
/>
<TextInput
value={beritaState.berita.create.form.deskripsi}
onChange={(val) => {
beritaState.berita.create.form.deskripsi = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
beritaState.berita.create.form.content = val
beritaState.berita.create.create()
}} />
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<BeritaEditor
showSubmit={false}
onEditorReady={(ed) => setEditorInstance(ed)}
/>
</Box>
<Button onClick={handleSubmit}>Simpan Berita</Button>
</Stack>
</Paper>
</Box>
)
);
}
// function BeritaList() {
// const beritaState = useProxy(stateDashboardBerita)
// useShallowEffect(() => {
// beritaState.berita.findMany.load()
// }, [])
// const router = useRouter()
// if (!beritaState.berita.findMany.data) return <Stack py={10}>
// {Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
// </Stack>
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <Stack>
// <Text fz={"xl"} fw={"bold"}>List Berita</Text>
// <SimpleGrid cols={{ base: 1, md: 4 }}>
// {beritaState.berita.findMany.data?.map((item) => (
// <Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
// <Box >
// <Flex justify="flex-end" mt={10}>
// <ActionIcon
// onClick={() => beritaState.berita.delete.byId(item.id)}
// disabled={beritaState.berita.delete.loading}
// color={colors['blue-button']} variant='transparent'>
// <IconX size={20} />
// </ActionIcon>
// <ActionIcon onClick={() => {
// router.push("/desa/berita/edit");
// }} color={colors['blue-button']} variant='transparent'>
// <IconEdit size={20} />
// </ActionIcon>
// </Flex>
// <Text fw={"bold"} fz={"sm"}>
// Kategori
// </Text>
// <Text>{item.kategoriBerita?.name}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Judul
// </Text>
// <Text>{item.judul}</Text>
// <Text lineClamp={1} fw={"bold"} fz={"sm"}>
// Deskripsi
// </Text>
// <Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Gambar
// </Text>
// <Image w={{ base: 100, md: 150 }} src={item.image?.link} alt="gambar" />
// </Box>
// </Paper>
// ))}
// </SimpleGrid>
// </Stack>
// </Paper>
// </Box>
// )
// }
function BeritaList() {
const beritaState = useProxy(stateDashboardBerita)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
beritaState.berita.findMany.load()
}, [])
if (!beritaState.berita.findMany.data) return <Stack py={10}>
{Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
</Stack>
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!beritaState.berita.findMany.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>List Berita</Text>
{beritaState.berita.findMany.data?.map((item) => (
<Text key={item.id}>{item.judul}</Text>
))}
<SimpleGrid cols={{ base: 1, md: 4 }}>
{beritaState.berita.findMany.data?.map((item) => (
<Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
<Box>
<Flex justify="flex-end" mt={10}>
<ActionIcon
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}
disabled={beritaState.berita.delete.loading}
color={colors['blue-button']} variant='transparent'
>
<IconX size={20} />
</ActionIcon>
<ActionIcon
onClick={() => router.push(`/admin/desa/berita/edit/${item.id}`)}
color={colors['blue-button']} variant='transparent'
>
<IconEdit size={20} />
</ActionIcon>
</Flex>
<Text fw={"bold"} fz={"sm"}>Kategori</Text>
<Text>{item.kategoriBerita?.name}</Text>
<Text fw={"bold"} fz={"sm"}>Judul</Text>
<Text>{item.judul}</Text>
<Text lineClamp={1} fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={item.image?.link} alt="gambar" />
</Box>
</Paper>
))}
</SimpleGrid>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
)
}
function SelectCategory({ onChange }: {
onChange: (value: Prisma.KatagoryBeritaGetPayload<{
select: {
name: true,
id: true
}
}>) => void
}) {
const beritaState = useProxy(stateDashboardBerita)
useShallowEffect(() => {
beritaState.category.findMany.load()
}, [])
if (!beritaState.category.findMany.data) return <Skeleton h={40} />
return <Group>
<Select placeholder='pilih katagori' label={<Text fz={"sm"} fw={"bold"}>Pilih Kategori</Text>} data={beritaState.category.findMany.data.map((item) => ({
value: item.id,
label: item.name
}))} onChange={(v) => {
const data = beritaState.category.findMany.data?.find((item) => item.id === v)
if (!data) return
onChange(data)
}} />
</Group>
function SelectCategory({
onChange,
}: {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}>) => void;
}) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load();
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
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,
}))}
onChange={(val) => {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
}}
searchable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
export default Page;

View File

@@ -1,10 +1,46 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function Page() {
return (
<div>
demografi-pekerjaan
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Demografi Pekerjaan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder="Masukkan jumlah pekerja laki - laki"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Perempuan</Text>}
placeholder="Masukkan jumlah pekerja perempuan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pekerjaan</Text>}
placeholder="Masukkan nama pekerjaan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Demografi Pekerjaan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,10 +1,42 @@
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import colors from '@/con/colors';
import React from 'react';
function Page() {
return (
<div>
jumlah-penduduk-miskin-2024-2025
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper p={"md"} bg={colors['white-1']} w={{ base: '100%', md: '50%' }}>
<Stack gap={"xs"}>
<Title order={3}>Jumlah Penduduk Miskin 2024-2025</Title>
<TextInput
label="Tahun"
placeholder="masukkan tahun"
/>
<TextInput
label="Jumlah Penduduk Miskin"
placeholder="masukkan jumlah penduduk miskin"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']} >
<Stack gap={"xs"}>
<Title order={3}>Grafik Jumlah Penduduk Miskin 2024-2025</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,11 +1,37 @@
import React from 'react';
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import DataPengangguran from './ui/dataPengangguran/page';
import GrafikDataPengangguran from './ui/grafikDataPengangguran/page';
import DetailDataPengangguran from './ui/detailDataPengangguran/page';
function Page() {
return (
<div>
jumlah-pengangguran-2024-2025
</div>
);
<Stack>
<Title order={3}>Jumlah Pengangguran 2024-2025</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Data Pengangguran Desa"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Data Pengangguran Desa">
Data Pengangguran Desa
</TabsTab>
<TabsTab value="Grafik Data Pengangguran Desa">
Grafik Data Pengangguran Desa
</TabsTab>
<TabsTab value="Detail Data Pengangguran Desa">
Detail Data Pengangguran Desa
</TabsTab>
</TabsList>
<TabsPanel value="Data Pengangguran Desa">
<DataPengangguran/>
</TabsPanel>
<TabsPanel value="Grafik Data Pengangguran Desa">
<GrafikDataPengangguran/>
</TabsPanel>
<TabsPanel value="Detail Data Pengangguran Desa">
<DetailDataPengangguran/>
</TabsPanel>
</Tabs>
</Stack>
)
}
export default Page;

View File

@@ -0,0 +1,51 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
function DataPengangguran() {
return (
<Box>
<Box py={15}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Data Pengangguran</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Total Pengangguran</Text>}
placeholder="masukkan total pengangguran"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Pengangguran Terdidik</Text>}
placeholder="masukkan pengangguran terdidik"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Usia Produktif</Text>}
placeholder="masukkan usia produktif"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Sedang Mencari Kerja</Text>}
placeholder="masukkan jumlah sedang mencari kerja"
/>
<Group>
<Button bg={colors['blue-button']}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>List Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Box>
);
}
export default DataPengangguran;

View File

@@ -0,0 +1,18 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
function DetailDataPengangguran() {
return (
<Box>
<Box py={15}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default DetailDataPengangguran;

View File

@@ -0,0 +1,49 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
function GrafikDataPengangguran() {
return (
<Box>
<Box py={15}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Data Pengangguran</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Total Pengangguran</Text>}
placeholder="masukkan total pengangguran"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Pengangguran Terdidik</Text>}
placeholder="masukkan pengangguran terdidik"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Usia Produktif</Text>}
placeholder="masukkan usia produktif"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Sedang Mencari Kerja</Text>}
placeholder="masukkan jumlah sedang mencari kerja"
/>
<Group>
<Button bg={colors['blue-button']}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>List Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</Box>
</Box>
);
}
export default GrafikDataPengangguran;

View File

@@ -1,10 +1,30 @@
import React from 'react';
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import ProgramKemiskinan from './ui/program/page';
import StatistikKemiskinan from './ui/statistik/page';
function Page() {
return (
<div>
program-kemiskinan
</div>
<Stack>
<Title order={3}>Program Kemiskinan</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Program Kemiskinan"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Program Kemiskinan">
Program Kemiskinan
</TabsTab>
<TabsTab value="Statistik Kemiskinan">
Statistik Kemiskinan
</TabsTab>
</TabsList>
<TabsPanel value="Program Kemiskinan">
<ProgramKemiskinan />
</TabsPanel>
<TabsPanel value="Statistik Kemiskinan">
<StatistikKemiskinan />
</TabsPanel>
</Tabs>
</Stack>
);
}

View File

@@ -0,0 +1,43 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function ProgramKemiskinan() {
return (
<Box py={15}>
<SimpleGrid cols={{base: 1, md: 2}}>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Program Kemiskinan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program</Text>}
placeholder="Masukkan nama program"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Program</Text>}
placeholder="Masukkan deskripsi program"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>List Data Program Kemiskinan</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default ProgramKemiskinan;

View File

@@ -0,0 +1,43 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function StatistikKemiskinan() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<Box>
<Paper w={{base: '100%', md: '50%'}} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Statistik Kemiskinan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="Masukkan tahun"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Penduduk Miskin</Text>}
placeholder="Masukkan jumlah penduduk miskin"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Statistik Kemiskinan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default StatistikKemiskinan;

View File

@@ -1,10 +1,31 @@
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import React from 'react';
import DataSektorUnggulan from './ui/data_sektor_unggulan/page';
import GrafikDataSektor from './ui/grafik_data_sektor/page';
function Page() {
return (
<div>
sektor-unggulan-desa
</div>
<Stack>
<Title order={3}>Sektor Unggulan Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Data Sektor Unggulan Desa"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Data Sektor Unggulan Desa">
Data Sektor Unggulan Desa
</TabsTab>
<TabsTab value="Grafik Data Sektor Unggulan Desa">
Grafik Data Sektor Unggulan Desa
</TabsTab>
</TabsList>
<TabsPanel value="Data Sektor Unggulan Desa">
<DataSektorUnggulan />
</TabsPanel>
<TabsPanel value="Grafik Data Sektor Unggulan Desa">
<GrafikDataSektor />
</TabsPanel>
</Tabs>
</Stack>
);
}

View File

@@ -0,0 +1,45 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function DataSektorUnggulan() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Data Sektor Unggulan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Sektor Unggulan</Text>}
placeholder="Masukkan nama sektor unggulan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Sektor Unggulan</Text>}
placeholder="Masukkan deskripsi sektor unggulan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Data Sektor Unggulan</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default DataSektorUnggulan;

View File

@@ -0,0 +1,44 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function GrafikDataSektor() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<Box>
<Paper w={{base: '100%', md: '50%'}} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Data Sektor Unggulan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Sektor Unggulan</Text>}
placeholder="Masukkan nama sektor unggulan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Sektor Unggulan</Text>}
placeholder="Masukkan jumlah sektor unggulan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Sektor Unggulan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default GrafikDataSektor;

View File

@@ -1,10 +1,49 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function Page() {
return (
<div>
desa-digital-smart-village
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Desa Digital Smart Village</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Desa Digital Smart Village</Text>}
placeholder="Masukkan nama desa digital smart village"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Desa Digital Smart Village</Text>}
placeholder="Masukkan deskripsi desa digital smart village"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>
Upload Gambar Desa Digital Smart Village
</Text>
<IconImageInPicture size={24} />
</Box>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Desa Digital Smart Village</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,10 +1,40 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function Page() {
return (
<div>
layanan-online-desa
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={'xs'}>
<Title order={3}>Layanan Online Desa</Title>
<TextInput
label={<Text fz={'sm'} fw={'bold'}>Nama Layanan</Text>}
placeholder="Masukkan nama layanan"
/>
<TextInput
label={<Text fz={'sm'} fw={'bold'}>Deskripsi Layanan</Text>}
placeholder="Masukkan deskripsi layanan"
/>
<Box>
<Text fz={'sm'} fw={'bold'}>Upload Gambar Layanan</Text>
<IconImageInPicture size={24} />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={'xs'}>
<Title order={3}>List Data Layanan Online Desa</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -36,7 +36,6 @@ function GrafikHasilKepuasan() {
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
w={{ base: '100%', md: '50%' }}
label="Label"
placeholder="masukkan label"
value={grafikHasilKepuasan.create.form.label}
@@ -45,7 +44,6 @@ function GrafikHasilKepuasan() {
}}
/>
<TextInput
w={{ base: '100%', md: '50%' }}
label="Jumlah Kepuasan"
type="number"
placeholder="masukkan jumlah kepuasan"

View File

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

View File

@@ -6,13 +6,14 @@ type FormCreate = Prisma.BeritaGetPayload<{
select: {
judul: true;
deskripsi: true;
image: true;
content: true;
katagoryBeritaId: true;
kategoriBeritaId: true;
imageId: true;
};
}>;
async function beritaCreate(context: Context) {
const body = context.body as FormCreate;
console.log(body)
// console.log(body)
@@ -20,9 +21,9 @@ async function beritaCreate(context: Context) {
data: {
content: body.content,
deskripsi: body.deskripsi,
image: body.image,
imageId: body.imageId,
judul: body.judul,
katagoryBeritaId: body.katagoryBeritaId,
kategoriBeritaId: body.kategoriBeritaId,
},
});

View File

@@ -0,0 +1,55 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const beritaDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const berita = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
},
});
if (!berita) {
return {
status: 404,
body: "Berita tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (berita.image) {
try {
const filePath = path.join(berita.image.path, berita.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: berita.image.id },
});
} catch (err) {
console.error("Gagal hapus file image:", err);
}
}
// Hapus berita dari DB
await prisma.berita.delete({
where: { id },
});
return {
success: true,
message: "Berita dan file terkait berhasil dihapus",
};
};
export default beritaDelete;

View File

@@ -0,0 +1,65 @@
import prisma from "@/lib/prisma";
export default async function handler(
request: Request
) {
// Extract the ID from the URL path
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
// Validate that the ID is a valid UUID or whatever format you're using
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Berita tidak ditemukan",
}, { status: 404 });
}
// Ensure we're returning a proper Response object
return new Response(JSON.stringify({
success: true,
message: "Success fetch berita by ID",
data,
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (e) {
console.error("Find by ID error:", e);
return new Response(JSON.stringify({
success: false,
message: "Gagal mengambil berita: " + (e instanceof Error ? e.message : 'Unknown error'),
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
}

View File

@@ -1,8 +1,27 @@
import prisma from "@/lib/prisma";
export default async function beritaFindMany() {
const res = await prisma.berita.findMany();
return {
data: res,
};
async function beritaFindMany() {
try {
const data = await prisma.berita.findMany({
where: { isActive: true },
include: {
image: true,
kategoriBerita: true,
},
});
return {
success: true,
message: "Success fetch berita",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch berita",
};
}
}
export default beritaFindMany;

View File

@@ -1,19 +1,43 @@
import Elysia, { t } from "elysia";
import katagoryBeritaFindMany from "./category";
import kategoriBeritaFindMany from "./category";
import beritaFindMany from "./find-many";
import beritaCreate from "./create";
import beritaDelete from "./del";
import beritaUpdate from "./updt";
import findBeritaById from "./find-by-id";
const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
.get("/category/find-many", katagoryBeritaFindMany)
.get("/category/find-many", kategoriBeritaFindMany)
.get("/find-many", beritaFindMany)
.get("/:id", async (context) => {
const response = await findBeritaById(new Request(context.request));
return response;
})
.post("/create", beritaCreate, {
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
image: t.String(),
imageId: t.String(),
content: t.String(),
katagoryBeritaId: t.Union([t.String(), t.Null()]),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
});
})
.delete("/delete/:id", beritaDelete)
.put(
"/:id",
async (context) => {
const response = await beritaUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
imageId: t.String(),
content: t.String(),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
}
);
export default Berita;

View File

@@ -0,0 +1,168 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { Prisma } from "@prisma/client";
import fs from "fs/promises";
import path from "path";
type FormUpdate = Prisma.BeritaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
content: true;
kategoriBeritaId: true;
imageId: true;
};
}>;
// async function beritaUpdate(context: Context) {
// const body = (await context.body) as FormUpdate;
// const {
// id,
// judul,
// deskripsi,
// content,
// kategoriBeritaId,
// imageId,
// } = body;
// if (!id) {
// return {
// status: 400,
// body: "ID tidak boleh kosong",
// };
// }
// const existing = await prisma.berita.findUnique({
// where: { id },
// include: {
// image: true,
// },
// });
// if (!existing) {
// return {
// status: 404,
// body: "Berita tidak ditemukan",
// };
// }
// // Cek jika imageId diubah
// if (existing.imageId && existing.imageId !== imageId) {
// const oldImage = existing.image;
// if (oldImage) {
// try {
// const filePath = path.join(oldImage.path, oldImage.name);
// await fs.unlink(filePath); // hapus file dari filesystem
// await prisma.fileStorage.delete({
// where: { id: oldImage.id }, // hapus record dari DB
// });
// } catch (err) {
// console.error("Gagal hapus gambar lama:", err);
// }
// }
// }
// const updated = await prisma.berita.update({
// where: { id },
// data: {
// judul,
// deskripsi,
// content,
// kategoriBeritaId,
// imageId,
// },
// });
// return {
// success: true,
// message: "Berita berhasil diupdate (gambar lama juga dihapus jika ada)",
// data: updated,
// };
// }
// export default beritaUpdate;
async function beritaUpdate(context: Context) {
try {
const id = context.params?.id as string; // ambil dari URL
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
judul,
deskripsi,
content,
kategoriBeritaId,
imageId,
} = body;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID tidak boleh kosong" }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const existing = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({ success: false, message: "Berita tidak ditemukan" }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.berita.update({
where: { id },
data: {
judul,
deskripsi,
content,
kategoriBeritaId: kategoriBeritaId || null,
imageId,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Berita berhasil diupdate",
data: updated,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error("Error updating berita:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate berita",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
export default beritaUpdate;

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
const fileStorageDelete = async (context: Context) => {
const { params } = context;
const id = params?.id as string;
if (!id) {
return {
status: 400,
body: "ID file tidak ditemukan",
};
}
if (!UPLOAD_DIR) {
return {
status: 500,
body: "UPLOAD_DIR belum dikonfigurasi",
};
}
// Cek file dari database
const file = await prisma.fileStorage.findUnique({
where: { id },
});
if (!file) {
return {
status: 404,
body: "File tidak ditemukan di database",
};
}
const filePath = path.join(file.path, file.name);
try {
// Hapus file dari filesystem
await fs.unlink(filePath);
} catch (err) {
console.error("Gagal hapus file:", err);
// Tetap lanjutkan hapus dari database meskipun file fisik tidak ditemukan
}
// Hapus dari database
await prisma.fileStorage.delete({
where: { id },
});
return {
message: "File berhasil dihapus",
deletedId: id,
};
};
export default fileStorageDelete;

View File

@@ -2,6 +2,7 @@ import Elysia, { t } from "elysia";
import fileStorageCreate from "./_lib/create";
import fileStorageFindUnique from "./_lib/findUniq";
import { fileStorageFindMany } from "./_lib/findMany";
import fileStorageDelete from "./_lib/del";
const FileStorage = new Elysia({
prefix: "/api/fileStorage",
@@ -14,6 +15,7 @@ const FileStorage = new Elysia({
}),
})
.get("/findUnique/:name", fileStorageFindUnique)
.get("/findMany", fileStorageFindMany);
.get("/findMany", fileStorageFindMany)
.delete("/delete/:id", fileStorageDelete);
export default FileStorage;