API & UI Menu Landing Page, Submenu Profile, Tabs 1 & 2 sisa tabs 3
This commit is contained in:
@@ -0,0 +1,270 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconAlertCircle, IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
function EditPejabatDesa() {
|
||||||
|
const allState = useProxy(profileLandingPageState.pejabatDesa);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// UI States
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) {
|
||||||
|
toast.error("ID tidak valid");
|
||||||
|
router.push("/admin/landing-page/profile/pejabat-desa");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id);
|
||||||
|
profileLandingPageState.pejabatDesa.edit.initialize(profileData);
|
||||||
|
|
||||||
|
|
||||||
|
if (profileData && profileData.image?.link) {
|
||||||
|
setPreviewImage(profileData.image.link);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading profile:", error);
|
||||||
|
toast.error("Gagal memuat data profile");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
profileLandingPageState.pejabatDesa.edit.reset(); // cleanup form
|
||||||
|
};
|
||||||
|
}, [params?.id, router]);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: string, value: string) => {
|
||||||
|
profileLandingPageState.pejabatDesa.edit.updateField(field as any, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (newFile: File | null) => {
|
||||||
|
if (!newFile) {
|
||||||
|
setFile(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFile(newFile);
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
setPreviewImage(event.target?.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(newFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting || !profileLandingPageState.pejabatDesa.edit.form.name.trim()) {
|
||||||
|
toast.error("Nama wajib diisi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload file jika ada
|
||||||
|
if (file) {
|
||||||
|
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||||
|
const uploaded = uploadResponse.data?.data;
|
||||||
|
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
toast.error("Gagal upload gambar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
profileLandingPageState.pejabatDesa.edit.form.imageId = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
const success = await profileLandingPageState.pejabatDesa.edit.submit();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success("Berhasil menyimpan perubahan");
|
||||||
|
router.push("/admin/landing-page/profile/pejabat-desa");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting form:", error);
|
||||||
|
toast.error("Gagal menyimpan profile");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (allState.edit.loading) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Center h={400}>
|
||||||
|
<Text>Memuat data profile...</Text>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (allState.edit.error) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||||
|
<Text fw="bold">Error</Text>
|
||||||
|
<Text>{allState.edit.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Box>
|
||||||
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Title order={3}>Edit Profile Pejabat Desa</Title>
|
||||||
|
|
||||||
|
{/* Nama Field */}
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw="bold">Nama Perbekel</Text>}
|
||||||
|
placeholder="Masukkan nama perbekel"
|
||||||
|
value={allState.edit.form.name}
|
||||||
|
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
||||||
|
error={!allState.edit.form.name && "Nama wajib diisi"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Posisi Field */}
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw="bold">Posisi</Text>}
|
||||||
|
placeholder="Masukkan posisi"
|
||||||
|
value={allState.edit.form.position}
|
||||||
|
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
|
||||||
|
error={!allState.edit.form.position && "Posisi wajib diisi"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => handleFileChange(files[0])}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Preview Gambar */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
||||||
|
{previewImage ? (
|
||||||
|
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
||||||
|
) : (
|
||||||
|
<Center w={200} h={200} bg="gray.2">
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<IconImageInPicture size={48} color="gray" />
|
||||||
|
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || allState.edit.loading}
|
||||||
|
disabled={!allState.edit.form.name}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={isSubmitting || allState.edit.loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPejabatDesa;
|
||||||
@@ -1,11 +1,104 @@
|
|||||||
import React from 'react';
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
|
const router = useRouter()
|
||||||
|
const allList = useProxy(profileLandingPageState.pejabatDesa)
|
||||||
|
useShallowEffect(() => {
|
||||||
|
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!allList.findUnique.data) {
|
||||||
|
return <Stack>
|
||||||
|
<Skeleton radius={10} h={800} />
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataArray = Array.isArray(allList.findUnique.data)
|
||||||
|
? allList.findUnique.data
|
||||||
|
: [allList.findUnique.data];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
Page
|
<Stack gap={"xs"}>
|
||||||
</div>
|
<Grid>
|
||||||
);
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
|
<Title order={3}>Preview Pejabat Desa</Title>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
|
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</Button>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
{dataArray.map((item) => (
|
||||||
|
<Box key={item.id} >
|
||||||
|
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||||
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
<Grid>
|
||||||
|
<GridCol span={{ base: 12, md: 12 }}>
|
||||||
|
<Center>
|
||||||
|
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
||||||
|
</Center>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 12 }}>
|
||||||
|
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Divider my={"md"} color={colors['blue-button']} />
|
||||||
|
{/* biodata perbekel */}
|
||||||
|
<Box px={{ base: 0, md: 50 }} pb={30}>
|
||||||
|
<Box pb={20} px={{ base: 0, md: 50 }}>
|
||||||
|
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Center>
|
||||||
|
<Image
|
||||||
|
pt={{ base: 0, md: 90 }}
|
||||||
|
src={item.image?.link || "/perbekel.png"}
|
||||||
|
w={{ base: 250, md: 350 }}
|
||||||
|
alt='Foto Profil PPID'
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "/perbekel.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Paper
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
py={20}
|
||||||
|
className="glass3"
|
||||||
|
px={{ base: 10, md: 10 }}
|
||||||
|
|
||||||
|
>
|
||||||
|
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
<Box pt={10}>
|
||||||
|
<Box>
|
||||||
|
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text>
|
||||||
|
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -1,23 +1,9 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
type FormUpdate = Prisma.PejabatDesaGetPayload<{
|
|
||||||
select: {
|
|
||||||
id: true;
|
|
||||||
name: true;
|
|
||||||
position: true;
|
|
||||||
imageId: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
export default async function pejabatDesaFindUnique(context: Context) {
|
export default async function pejabatDesaFindUnique(context: Context) {
|
||||||
try {
|
try {
|
||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
const body = (await context.body) as Omit<FormUpdate, "id">;
|
|
||||||
|
|
||||||
const { name, position, imageId } = body;
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -35,12 +21,8 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existing = await prisma.pejabatDesa.findUnique({
|
const existing = await prisma.pejabatDesa.findUnique({
|
||||||
where: {
|
where: { id },
|
||||||
id,
|
include: { image: true },
|
||||||
},
|
|
||||||
include: {
|
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -58,37 +40,11 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (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.pejabatDesa.update({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Data pejabat desa berhasil ditemukan",
|
message: "Data pejabat desa berhasil ditemukan",
|
||||||
data: updated,
|
data: existing,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -98,11 +54,11 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pejabat desa:", error);
|
console.error("Error fetching pejabat desa:", error);
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Terjadi kesalahan saat mengupdate pejabat desa",
|
message: "Terjadi kesalahan saat mengambil data pejabat desa",
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
|
|||||||
Reference in New Issue
Block a user