Merge pull request #24 from bipproduction/nico/30-may-25

Jum'at, 30 May 2025 : 
Yang Sudah Di Kerjakan
- Tampilan UI Admin di menu inovasi
- API Create, edit dan delete potensi
- Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
- Progress Tampilan UI Admin Di Menu lingkungan
- Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
- API Create, edit dan delete pengumuman
- Tampilan UI Admin Di Menu Pendidikan
This commit is contained in:
2025-05-30 21:15:27 +08:00
committed by GitHub
47 changed files with 2567 additions and 487 deletions

View File

@@ -47,6 +47,23 @@ model AppMenuChild {
appMenuId String?
}
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
}
//========================================= MENU PPID ========================================= //
// ========================================= VISI MISI PPID ========================================= //
model VisiMisiPPID {
@@ -263,6 +280,21 @@ model KategoriBerita {
isActive Boolean @default(true)
}
// ========================================= POTENSI DESA ========================================= //
model PotensiDesa {
id String @id @default(cuid())
name String
deskripsi String
kategori 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)
}
// ========================================= PENGUMUMAN ========================================= //
model Pengumuman {
id String @id @default(cuid())
@@ -583,19 +615,3 @@ model DoctorSign {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// === BARU
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
}

View File

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -0,0 +1,85 @@
// TestEditor.tsx
import { RichTextEditor, Link } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
type CreateEditorProps = {
value: string;
onChange: (content: string) => void;
};
export default function CreateEditor({ value, onChange }: CreateEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: value,
onUpdate: () => {
if (editor) {
onChange(editor.getHTML());
}
},
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}

View File

@@ -0,0 +1,102 @@
'use client'
import { RichTextEditor, Link } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { useEffect } from 'react';
type EditEditorProps = {
value: string;
onChange: (content: string) => void;
};
export default function EditEditor({ value, onChange }: EditEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: value,
// Hapus `immediatelyRender` dan `onMount`
});
// Sinkronisasi konten saat `value` berubah
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value);
}
}, [value, editor]);
// Sinkronisasi konten ke parent saat diubah
useEffect(() => {
if (!editor) return;
const updateHandler = () => onChange(editor.getHTML());
editor.on('update', updateHandler);
return () => {
editor.off('update', updateHandler);
};
}, [editor, onChange]);
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
{/* Toolbar seperti sebelumnya */}
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react'; // Sesuaikan jika kamu pakai icon lain
import colors from '@/con/colors';
const HeaderSearch = ({ title = "", placeholder = "pencarian", searchIcon = <IconSearch size={20} /> }: { title: string, placeholder?: string, searchIcon?: React.ReactNode }) => {
return (
<Grid>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>{title}</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius={"lg"} bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
/>
</Paper>
</GridCol>
</Grid>
);
};
export default HeaderSearch;

View File

@@ -0,0 +1,30 @@
'use client'
import colors from '@/con/colors';
import { Grid, GridCol, Button, Text } from '@mantine/core';
import { IconCircleDashedPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
const JudulList = ({ title = "", href = "#" }) => {
const router = useRouter();
const handleNavigate = () => {
router.push(href);
};
return (
<Grid align="center" mb={10}>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>{title}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }} ta="right">
<Button onClick={handleNavigate} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
);
};
export default JudulList;

View File

@@ -1,4 +1,3 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -57,10 +56,10 @@ const berita = proxy({
);
if (res.status === 200) {
berita.findMany.load();
return toast.success("success create");
return toast.success("Berita berhasil disimpan!");
}
return toast.error("failed create");
return toast.error("Gagal menyimpan berita");
} catch (error) {
console.log((error as Error).message);
} finally {
@@ -88,6 +87,30 @@ const berita = proxy({
}
},
},
findUnique: {
data: null as
| Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/berita/${id}`);
if (res.ok) {
const data = await res.json();
berita.findUnique.data = data.data ?? null;
} else {
console.error('Failed to fetch berita:', res.statusText);
berita.findUnique.data = null;
}
} catch (error) {
console.error('Error fetching berita:', error);
berita.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {

View File

@@ -0,0 +1,223 @@
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 templateForm = z.object({
name: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(50),
kategori: z.string().min(1).max(50),
imageId: z.string().min(1).max(50),
content: z.string().min(1).max(5000),
})
const defaultForm = {
name: "",
deskripsi: "",
kategori: "",
imageId: "",
content: "",
}
const potensiDesaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(potensiDesaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
potensiDesaState.create.loading = true;
const res = await ApiFetch.api.desa.potensi["create"].post(potensiDesaState.create.form);
if (res.status === 200) {
potensiDesaState.findMany.load();
return toast.success("Potensi berhasil disimpan!");
}
return toast.error("Gagal menyimpan potensi");
} catch (error) {
console.log((error as Error).message);
} finally {
potensiDesaState.create.loading = false;
}
}
},
findMany: {
data: null as
| Prisma.PotensiDesaGetPayload<{
include: {
image: true;
}
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.potensi["find-many"].get();
if (res.status === 200) {
potensiDesaState.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as
| Prisma.PotensiDesaGetPayload<{
include: {
image: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/potensi/${id}`);
if (res.ok) {
const data = await res.json();
potensiDesaState.findUnique.data = data.data ?? null;
} else {
console.error('Failed to fetch potensi:', res.statusText);
potensiDesaState.findUnique.data = null;
}
} catch (error) {
console.error('Error fetching potensi:', error);
potensiDesaState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
potensiDesaState.delete.loading = true;
const response = await fetch(`/api/desa/potensi/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Potensi berhasil dihapus");
await potensiDesaState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus potensi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus potensi");
} finally {
potensiDesaState.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/potensi/${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,
deskripsi: data.deskripsi,
kategori: data.kategori,
imageId: data.imageId || "",
content: data.content,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading potensi:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(potensiDesaState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
potensiDesaState.edit.loading = true;
const response = await fetch(`/api/desa/potensi/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
kategori: this.form.kategori,
imageId: this.form.imageId,
content: this.form.content,
}),
});
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 potensi");
await potensiDesaState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update potensi");
}
} catch (error) {
console.error("Error updating potensi:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update potensi");
return false;
} finally {
potensiDesaState.edit.loading = false;
}
},
reset() {
potensiDesaState.edit.id = "";
potensiDesaState.edit.form = { ...defaultForm };
}
}
})
export default potensiDesaState

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import {
@@ -15,29 +14,27 @@ import {
Title,
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { useProxy } from "valtio/utils";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
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 stateDashboardBerita from "../../../../_state/desa/berita";
import { Prisma } from "@prisma/client";
import { useShallowEffect } from "@mantine/hooks";
import { BeritaEditor } from "../../_com/BeritaEditor";
import colors from "@/con/colors";
import { Prisma } from "@prisma/client";
import stateDashboardBerita from "../../../../_state/desa/berita";
function BeritaEdit() {
function EditBerita() {
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 || '',
@@ -51,7 +48,7 @@ function BeritaEdit() {
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) {
@@ -62,7 +59,7 @@ function BeritaEdit() {
content: data.content || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
@@ -72,57 +69,28 @@ function BeritaEdit() {
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
...beritaState.berita.edit.form,
judul: formData.judul,
deskripsi: formData.deskripsi,
content: formData.content,
kategoriBeritaId: formData.kategoriBeritaId || '',
imageId: formData.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 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");
}
@@ -131,7 +99,6 @@ function BeritaEdit() {
beritaState.berita.edit.form.imageId = uploaded.id;
}
await beritaState.berita.edit.update();
toast.success("Berita berhasil diperbarui!");
router.push("/admin/desa/berita");
@@ -142,14 +109,18 @@ function BeritaEdit() {
};
return (
<Box py={10}>
<IconArrowBack color={colors["blue-button"]} size={30} onClick={() => router.back()}/>
<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 Berita</Title>
<TextInput
value={formData.judul}
onChange={(e) => setFormData({...formData, judul: e.target.value})}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
@@ -166,7 +137,7 @@ function BeritaEdit() {
<TextInput
value={formData.deskripsi}
onChange={(e) => setFormData({...formData, deskripsi: e.target.value})}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
@@ -194,15 +165,16 @@ function BeritaEdit() {
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<BeritaEditor
initialContent={formData.content}
onEditorReady={handleEditorReady}
showSubmit={false}
onUpdate={(content) => setFormData({...formData, content})}
<EditEditor
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
beritaState.berita.edit.form.content = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan Perubahan</Button>
<Button onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>
</Box>
@@ -237,9 +209,9 @@ function SelectCategory({
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
@@ -266,4 +238,4 @@ function SelectCategory({
);
}
export default BeritaEdit;
export default EditBerita;

View File

@@ -0,0 +1,120 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, 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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita';
function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
beritaState.berita.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/berita")
}
}
if (!beritaState.berita.findUnique.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} 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 Berita</Text>
{beritaState.berita.findUnique.data ? (
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (beritaState.berita.findUnique.data) {
setSelectedId(beritaState.berita.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (beritaState.berita.findUnique.data) {
router.push(`/admin/desa/berita/${beritaState.berita.findUnique.data.id}/edit`);
}
}}
disabled={!beritaState.berita.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 berita ini?'
/>
</Box>
);
}
export default DetailBerita;

View File

@@ -0,0 +1,168 @@
'use client'
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 { 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);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = {
judul: "",
deskripsi: "",
kategoriBeritaId: "",
imageId: "",
content: "",
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// 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();
// Reset form setelah submit
resetForm();
router.push("/admin/desa/berita")
};
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Berita</Title>
<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"
/>
<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>
<CreateEditor
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
beritaState.berita.create.form.content = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
</Stack>
</Paper>
</Box>
);
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"
/>
);
}
}

View File

@@ -1,146 +1,38 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
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 { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { IconEdit, IconImageInPicture, IconX } from '@tabler/icons-react';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } 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';
function Page() {
return (
<Box>
<Title order={3}>Berita</Title>
<BeritaCreate />
<Grid>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>Berita</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius={"lg"} bg={colors['white-1']}>
<TextInput
radius={"lg"}
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
/>
</Paper>
</GridCol>
</Grid>
<BeritaList />
</Box>
);
}
function BeritaCreate() {
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"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Berita</Title>
<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"
/>
<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 bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
</Stack>
</Paper>
</Box>
);
}
@@ -228,9 +120,7 @@ function BeritaList() {
if (!beritaState.berita.findMany.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
<Skeleton h={500} />
</Stack>
)
}
@@ -239,41 +129,49 @@ function BeritaList() {
<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={() => {
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>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>List Berita</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push("/admin/desa/berita/create")} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
<TableTh w={250}>Kategori</TableTh>
<TableTh w={250}>Image</TableTh>
<TableTh w={200}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody >
{beritaState.berita.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd >
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
</Box>
</TableTd>
<TableTd >{item.kategoriBerita?.name}</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/desa/berita/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table> </Box>
</Stack>
</Paper>
@@ -288,45 +186,4 @@ function BeritaList() {
)
}
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

@@ -0,0 +1,128 @@
'use client';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import potensiDesaState from '../../../_state/desa/potensi';
import { useRouter } from 'next/navigation';
import CreateEditor from '../../../_com/createEditor';
function CreatePotensi() {
const potensiState = useProxy(potensiDesaState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const handleSubmit = async () => {
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
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');
}
potensiState.create.form.imageId = uploaded.id;
await potensiState.create.create();
resetForm();
router.push('/admin/desa/potensi');
};
const resetForm = () => {
potensiState.create.form = {
name: '',
deskripsi: '',
kategori: '',
imageId: '',
content: '',
};
setPreviewImage(null);
setFile(null);
};
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Potensi</Title>
<TextInput
value={potensiState.create.form.name}
onChange={(val) => (potensiState.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={potensiState.create.form.deskripsi}
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
value={potensiState.create.form.kategori}
onChange={(val) => (potensiState.create.form.kategori = val.target.value)}
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
<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>
<CreateEditor
value={potensiState.create.form.content}
onChange={(htmlContent) => {
potensiState.create.form.content = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan Potensi
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePotensi;

View File

@@ -0,0 +1,121 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
import potensiDesaState from '../../../../_state/desa/potensi';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
export default function DetailPotensi() {
const router = useRouter()
const params = useParams()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const potensiState = useProxy(potensiDesaState)
useShallowEffect(() => {
potensiState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
potensiState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/potensi")
}
}
if (!potensiState.findUnique.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
{potensiState.findUnique.data ? (
<Paper key={potensiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.kategori}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.deskripsi}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={potensiState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: potensiState.findUnique.data.content }} />
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (potensiState.findUnique.data) {
setSelectedId(potensiState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={potensiState.delete.loading || !potensiState.findUnique.data}
color="red"
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (potensiState.findUnique.data) {
router.push(`/admin/desa/potensi/edit/${potensiState.findUnique.data.id}`)
}
}}
disabled={!potensiState.findUnique.data}
color="green"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/>
</Box>
);
}

View File

@@ -0,0 +1,169 @@
'use client'
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditPotensi() {
const potensiState = useProxy(potensiDesaState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
kategori: '',
content: '',
imageId: ''
});
useEffect(() => {
const loadPotensi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await potensiDesaState.edit.load(id); // ambil data dari API
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
kategori: data.kategori || '',
content: data.content || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading potensi:", error);
toast.error("Gagal memuat data potensi");
}
};
loadPotensi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Sinkronkan semua data dari formData ke state global
potensiState.edit.form = {
...potensiState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
kategori: formData.kategori,
content: formData.content,
};
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");
}
potensiState.edit.form.imageId = uploaded.id;
}
await potensiState.edit.update();
toast.success("Potensi berhasil diperbarui!");
router.push("/admin/desa/potensi");
} catch (error) {
console.error("Error updating potensi:", error);
toast.error("Terjadi kesalahan saat memperbarui potensi");
}
};
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Potensi</Title>
<TextInput
value={formData.name}
onChange={(e) => {
const val = e.target.value;
setFormData((prev) => ({ ...prev, name: val }));
potensiState.edit.form.name = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsi}
onChange={(e) => {
const val = e.target.value;
setFormData((prev) => ({ ...prev, deskripsi: val }));
potensiState.edit.form.deskripsi = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
value={formData.kategori}
onChange={(e) => {
const val = e.target.value;
setFormData((prev) => ({ ...prev, kategori: val }));
potensiState.edit.form.kategori = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="masukkan kategori"
/>
<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>
<EditEditor
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
potensiState.edit.form.content = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Potensi</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPotensi;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListPotensi() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Potensi Desa</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListPotensi;

View File

@@ -1,40 +1,89 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, TextInput, Title } from '@mantine/core';
import ListPotensi from './listPage';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import potensiDesaState from '../../_state/desa/potensi';
function Potensi() {
return (
<Box py={10}>
<Stack gap={"xs"}>
<Title order={3}>Potensi Desa</Title>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<TextInput
label="Nama Potensi"
placeholder='masukkan nama potensi'
/>
<TextInput
label="Kategori Potensi"
placeholder='masukkan kategori potensi'
/>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListPotensi />
</SimpleGrid>
</Stack>
<Box>
<HeaderSearch
title='Potensi Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPotensi />
</Box>
);
}
function ListPotensi() {
const potensiState = useProxy(potensiDesaState)
useShallowEffect(() => {
potensiState.findMany.load()
}, [])
const router = useRouter()
if (!potensiState.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 Potensi'
href='/admin/desa/potensi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{potensiState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box></TableTd>
<TableTd>{item.kategori}</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/potensi/detail/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default Potensi;

View File

@@ -0,0 +1,73 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
function CreateDataLingkunganDesa() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Data Lingkungan Desa</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
{/* <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>
{/* <CreateEditor
value={potensiState.create.form.content}
onChange={(htmlContent) => {
potensiState.create.form.content = htmlContent;
}}
/> */}
</Box>
<Button bg={colors['blue-button']} >
Simpan Data
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateDataLingkunganDesa;

View File

@@ -1,11 +1,66 @@
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
function Page() {
function DataLingkunganDesa() {
return (
<div>
data-lingkungan-desa
</div>
<Box>
<HeaderSearch
title='Data Lingkungan Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListDataLingkunganDesa/>
</Box>
);
}
export default Page;
function ListDataLingkunganDesa() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default DataLingkunganDesa;

View File

@@ -0,0 +1,61 @@
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function Page() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Edukasi Lingkungan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
{/* <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">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,11 +1,66 @@
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
function Page() {
function Page() {
return (
<div>
edukasi-lingkungan
</div>
<Box py={10}>
<HeaderSearch
title='Edukasi Lingkungan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListEdukasiLingkungan/>
</Box>
);
}
function ListEdukasiLingkungan() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Edukasi Lingkungan'
href='/admin/lingkungan/edukasi-lingkungan/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} src="/" alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default Page;

View File

@@ -0,0 +1,65 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
function Page() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Gotong Royong</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
{/* <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">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,11 +1,68 @@
import React from 'react';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
function Page() {
function GotongRoyong() {
return (
<div>
gotong-royong
</div>
<Box>
<HeaderSearch
title='Gotong Royong'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListGotongRoyong/>
</Box>
);
}
export default Page;
function ListGotongRoyong() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Gotong Royong'
href='/admin/lingkungan/gotong-royong/create'
/>
<Box style={{ overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>Kategori</TableTd>
<TableTd>
<Image w={100} src="/" alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default GotongRoyong;

View File

@@ -0,0 +1,62 @@
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function Page() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Konservasi Adat Bali</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
{/* <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">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,11 +1,66 @@
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
function Page() {
function KonservasiAdatBali() {
return (
<div>
konservasi-adat-bali
</div>
<Box py={10}>
<HeaderSearch
title='Konservasi Adat Bali'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListKonservasiAdatBali />
</Box>
);
}
export default Page;
function ListKonservasiAdatBali() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Konservasi Adat Bali'
href='/admin/lingkungan/konservasi-adat-bali/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} src="/" alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default KonservasiAdatBali;

View File

@@ -0,0 +1,71 @@
'use client'
import colors from "@/con/colors";
import { Box, Button, Paper, Stack, Title, TextInput, Text } from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
export default function CreatePengelolaanSampahBankSampah() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Mekanisme Bank Sampah</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
{/* <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>
{/* <CreateEditor
value={potensiState.create.form.content}
onChange={(htmlContent) => {
potensiState.create.form.content = htmlContent;
}}
/> */}
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
)
}

View File

@@ -1,11 +1,33 @@
import React from 'react';
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import ListPage from './ui/list_page/listPage';
import colors from '@/con/colors';
function Page() {
function PengelolaanSampahBankSampah() {
return (
<div>
pengelolaan-sampah-bank-sampah
</div>
);
<Box>
<Stack>
<Title order={3}>Pengelolaan Sampah Bank Sampah</Title>
<Tabs defaultValue="list" color={colors['blue-button']} variant="pills">
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="list">
List Pengelolaan Sampah Bank Sampah
</TabsTab>
<TabsTab value="maps">
Maps
</TabsTab>
</TabsList>
<TabsPanel value="list">
<ListPage />
</TabsPanel>
<TabsPanel value="maps">
Maps
</TabsPanel>
</Tabs>
</Stack>
</Box>
)
}
export default Page;
export default PengelolaanSampahBankSampah;

View File

@@ -0,0 +1,66 @@
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
function ListPage() {
return (
<Box py={10}>
<HeaderSearch
title='Mekanisme Bank Sampah'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPengelolaanSampahBankSampah />
</Box>
);
}
function ListPengelolaanSampahBankSampah() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Mekanisme Bank Sampah'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListPage;

View File

@@ -0,0 +1,73 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function Page() {
const router = useRouter()
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']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap={"xs"}>
<Title order={3}>Create Program Penghijauan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<Text fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
{/* <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>
{/* <CreateEditor
value={potensiState.create.form.content}
onChange={(htmlContent) => {
potensiState.create.form.content = htmlContent;
}}
/> */}
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
</Button>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,11 +1,68 @@
import React from 'react';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
function Page() {
function ProgramPenghijauan() {
return (
<div>
program-penghijauan
</div>
<Box>
<HeaderSearch
title='Program Penghijauan'
placeholder='pencarian'
searchIcon={<IconSearch size={20}/>}
/>
<ListManfaatPenghijauan/>
</Box>
);
}
export default Page;
function ListManfaatPenghijauan() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Manfaat Program Penghijauan'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Jumlah</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Jumlah</Text>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ProgramPenghijauan;

View File

@@ -42,7 +42,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
padding={'md'}
>
<AppShellHeader bg={colors["white-trans-1"]}>
<AppShellHeader bg={colors["white-1"]}>
<Group px={10} align="center">
{!desktopOpened && (
<ActionIcon variant="light" onClick={toggleDesktop}>

View File

@@ -13,9 +13,6 @@ type FormCreate = Prisma.BeritaGetPayload<{
}>;
async function beritaCreate(context: Context) {
const body = context.body as FormCreate;
console.log(body)
// console.log(body)
await prisma.berita.create({
data: {

View File

@@ -40,26 +40,20 @@ export default async function handler(
}
// Ensure we're returning a proper Response object
return new Response(JSON.stringify({
return Response.json({
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({
return Response.json({
success: false,
message: "Gagal mengambil berita: " + (e instanceof Error ? e.message : 'Unknown error'),
}), {
}, {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
}

View File

@@ -15,76 +15,6 @@ type FormUpdate = Prisma.BeritaGetPayload<{
};
}>;
// 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
@@ -155,7 +85,7 @@ async function beritaUpdate(context: Context) {
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error("Error updating berita:", error);
console.error("Error updating berita:", error);
return new Response(
JSON.stringify({
success: false,

View File

@@ -2,11 +2,12 @@ import Elysia from "elysia";
import Berita from "./berita";
import Pengumuman from "./pengumuman";
import ProfileDesa from "./profile/profile_desa";
import PotensiDesa from "./potensi";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(Berita)
.use(Pengumuman)
.use(ProfileDesa)
.use(PotensiDesa)
export default Desa;

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PotensiDesaGetPayload<{
select: {
name: true;
deskripsi: true;
kategori: true;
imageId: true;
content: true;
}
}>
export default async function potensiDesaCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.potensiDesa.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
kategori: body.kategori,
imageId: body.imageId,
content: body.content,
},
});
return {
success: true,
message: "Success create potensi desa",
data: {
...body,
},
};
}

View File

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

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
async function potensiDesaFindMany() {
try {
const data = await prisma.potensiDesa.findMany({
where: { isActive: true },
include: {
image: true,
},
});
return {
success: true,
message: "Success fetch potensi desa",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch potensi desa",
};
}
}
export default potensiDesaFindMany

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
export default async function findUnique(
request: Request) {
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 {
if( typeof id !== 'string') {
return Response.json({
success: false,
message: "ID harus berupa string",
}, { status: 400 });
}
const data = await prisma.potensiDesa.findUnique({
where: { id },
include: {
image: true,
},
});
if(!data) {
return Response.json({
success: false,
message: "Potensi Desa tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch potensi desa by ID",
data,
}, { status: 200 });
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,45 @@
import Elysia from "elysia";
import createPotensiDesa from "./create";
import { t } from "elysia";
import potensiDesaDelete from "./del";
import potensiDesaFindMany from "./find-many";
import potensiDesaUpdate from "./updt";
import findUnique from "./find-unique";
const PotensiDesa = new Elysia({
prefix: "/potensi", tags: ["Desa/Potensi"]
})
.post("/create", createPotensiDesa, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
kategori: t.String(),
imageId: t.String(),
content: t.String(),
}),
})
.delete("/del/:id", potensiDesaDelete)
.get("/find-many", potensiDesaFindMany)
.get("/:id", async (context) => {
const response = await findUnique(new Request(context.request));
return response;
})
.put(
"/:id",
async (context) => {
const response = await potensiDesaUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
kategori: t.String(),
imageId: t.String(),
content: t.String(),
}),
}
)
export default PotensiDesa

View File

@@ -0,0 +1,98 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.PotensiDesaGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
kategori: true;
imageId: true;
content: true;
};
}>;
export default async function potensiDesaUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
deskripsi,
kategori,
imageId,
content,
} = body;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID tidak ditemukan" }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
const existing = await prisma.potensiDesa.findUnique({
where: { id },
include: {
image: true,
}
});
if (!existing) {
return new Response(
JSON.stringify({ success: false, message: "Potensi Desa 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 (error) {
console.error("Gagal hapus gambar lama:", error);
}
}
}
const updated = await prisma.potensiDesa.update({
where: { id },
data: {
name,
deskripsi,
kategori,
imageId,
content,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Potensi Desa berhasil diupdate",
data: updated,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error("Error updating potensi desa:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate potensi desa",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}

View File

@@ -10,7 +10,7 @@ function Footer() {
return (
<>
<Stack bg={colors["blue-button"]}>
<Box w={mobile ? "100%" : "100%"} p={"xl"} h={{ base: 1850, md: 1100 }} >
<Box w={mobile ? "100%" : "100%"} p={"xl"} h={{ base: 2500, md: 1100 }} >
<Center>
<Paper w={"100%"}>
<Box component="footer" py="xl">

View File

@@ -44,9 +44,9 @@ function DesaAntiKorupsi() {
<Stack gap={"0"} bg={colors.Bg} p={"sm"} h={mobile ? 2000 : 1150}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Center>
<Text fz={"3.4rem"}>Desa Anti Korupsi</Text>
<Text fz={{base: "2.4rem", md: "3.4rem"}}>Desa Anti Korupsi</Text>
</Center>
<Text ta={"center"} fz={"1.4rem"}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Text ta={"center"} fz={{base: "1.2rem", md: "1.4rem"}}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Center py={20}>
<Button radius={"lg"} fz={"h4"} bg={colors["blue-button"]} component={Link} href={"/darmasaba/desaantikorupsi"}>Selengkapnya</Button>
</Center>
@@ -65,13 +65,13 @@ function DesaAntiKorupsi() {
<Paper p={"lg"} >
<Flex gap={"lg"} justify={"center"} align={"center"}>
<Box >
<Text fz={"lg"} ta={"center"} c={colors["blue-button"]}>{v.judul}</Text>
<Text fz={{base: "1.2rem", md: "lg"}} ta={"center"} c={colors["blue-button"]}>{v.judul}</Text>
<Flex justify={"center"} align={"center"}>
<Box>
{v.icon}
</Box>
<Box px={20}>
<Text fz={"sm"} ta={"justify"}>{v.deskripsi}</Text>
<Text fz={"sm"} ta={{base: "left", md: "justify"}}>{v.deskripsi}</Text>
</Box>
</Flex>
</Box>

View File

@@ -1,7 +1,8 @@
"use client";
import { Stack, Container, Center, Text, Paper, Flex, Box, SimpleGrid } from "@mantine/core";
import { BarChart, PieChart } from '@mantine/charts';
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Center, Container, Flex, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
const dataBarChart = [
{
@@ -71,13 +72,14 @@ const dataPieChart3 = [
]
function Kepuasan() {
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Center>
<Text fz={"3.4rem"}>Indeks Kepuasan Masyarakat</Text>
<Text ta={"center"} fz={{base: "2.4rem", md: "3.4rem"}}>Indeks Kepuasan Masyarakat</Text>
</Center>
<Text fz={"1.4rem"} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Text fz={{base: "1.2rem", md: "1.4rem"}} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
@@ -118,7 +120,7 @@ function Kepuasan() {
<Text fw={"bold"}>Jenis Kelamin</Text>
<Box py={"xl"}>
<PieChart
size={250}
size={isMobile ? 100 : 220}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
@@ -135,7 +137,7 @@ function Kepuasan() {
<Text fw={"bold"}>Pilihan</Text>
<Box py={"xl"}>
<PieChart
size={250}
size={isMobile ? 100 : 220}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
@@ -152,7 +154,7 @@ function Kepuasan() {
<Text fw={"bold"}>Umur</Text>
<Box py={"xl"}>
<PieChart
size={250}
size={isMobile ? 100 : 220}
withLabelsLine
labelsPosition="outside"
labelsType="percent"

View File

@@ -5,9 +5,9 @@ import {
Card,
Flex,
Grid,
GridCol,
Image,
Paper,
SimpleGrid,
Stack,
Text
} from "@mantine/core";
@@ -58,10 +58,9 @@ function LandingPage() {
<Grid
>
<Grid.Col span={{
base: 2,
sm: 3,
base: 3,
lg: 2,
md: 3,
xl: 2
}}>
<Box
pos={"relative"}
@@ -88,10 +87,9 @@ function LandingPage() {
</Grid.Col>
<Grid.Col span={{
base: 2,
sm: 3,
base: 6,
lg: 2,
md: 3,
xl: 2
}}>
<Box
pos={"relative"}
@@ -118,10 +116,9 @@ function LandingPage() {
</Box>
</Grid.Col>
<Grid.Col span={{
base: 8,
sm: 12,
base: 12,
lg: 8,
md: 12,
xl: 8
}}>
<Paper
pos={"relative"}
@@ -130,15 +127,14 @@ function LandingPage() {
w={{ base: "100%", sm: "auto", md: "auto" }}
flex={{ base: "1", sm: "1", md: "1" }}
>
<SimpleGrid
cols={{
base: 2,
sm: 1,
md: 2,
}}
spacing={{ base: "xs", md: "md" }}
<Grid
>
<Box>
<GridCol span={{
base: 12,
lg: 6,
md: 6,
}}>
<Box>
<Text c={colors["white-1"]} fz={"sm"}>
Jadwal Kerja
</Text>
@@ -168,7 +164,14 @@ function LandingPage() {
</Flex>
</Paper>
</Box>
<Box>
</GridCol>
<GridCol span={{
base: 12,
lg: 6,
md: 6,
}}>
<Box>
<Text c={colors["white-1"]} fz={"sm"}>
Rabu, 10 Maret 2025
</Text>
@@ -187,7 +190,8 @@ function LandingPage() {
</Box>
</Paper>
</Box>
</SimpleGrid>
</GridCol>
</Grid>
</Paper>
</Grid.Col>

View File

@@ -51,7 +51,7 @@ function Penghargaan() {
<Text fz={"1.4rem"} c={"white"}>
Juara 2 Duta Investasi
</Text>
<Text fz={"1.4rem"} c={"white"}>
<Text fz={"1.2rem"} c={"white"}>
Juara Favorit Lomba Video Pendek
</Text>
</Stack>