Fix FileInput dengan Dropzone

This commit is contained in:
2025-08-14 10:24:03 +08:00
parent 212e2db1fb
commit c99416c7f8
26 changed files with 1451 additions and 696 deletions

View File

@@ -1,28 +1,28 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { import {
Box, Box,
Button, Button,
Center, Group,
Image, Image,
Paper, Paper,
Select, Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; 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 "@/app/admin/(dashboard)/_state/desa/berita";
function EditBerita() { function EditBerita() {
@@ -130,27 +130,62 @@ function EditBerita() {
placeholder="masukkan deskripsi" placeholder="masukkan deskripsi"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor <EditEditor

View File

@@ -3,9 +3,10 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -113,25 +114,62 @@ export default function CreateBerita() {
placeholder="masukkan deskripsi" placeholder="masukkan deskripsi"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <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>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor <CreateEditor

View File

@@ -4,10 +4,11 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -105,26 +106,62 @@ function EditPenghargaan() {
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara" placeholder="masukkan juara"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import ApiFetch from '@/lib/api-fetch';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import penghargaanState from '../../../_state/desa/penghargaan';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import penghargaanState from '../../../_state/desa/penghargaan';
function CreatePenghargaan() { function CreatePenghargaan() {
@@ -85,25 +86,62 @@ function CreatePenghargaan() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <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>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -45,20 +46,6 @@ function ProfilePerbekel() {
}; };
}, [params?.id, router]); }, [params?.id, router]);
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 () => { const handleSubmit = async () => {
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) { if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
@@ -128,27 +115,61 @@ function ProfilePerbekel() {
value={perbekelState.edit.form.biodata} value={perbekelState.edit.form.biodata}
onChange={(val) => perbekelState.edit.form.biodata = val} onChange={(val) => perbekelState.edit.form.biodata = val}
/> />
{/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box> <Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
{previewImage ? ( <Box>
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" /> <Dropzone
) : ( onDrop={(files) => {
<Center w={200} h={200} bg="gray.2"> const selectedFile = files[0]; // Ambil file pertama
<Stack align="center" gap="xs"> if (selectedFile) {
<IconImageInPicture size={48} color="gray" /> setFile(selectedFile);
<Text size="sm" c="gray">Tidak ada gambar</Text> setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
</Stack> }
</Center> }}
)} 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> </Box>
<Box> <Box>

View File

@@ -116,14 +116,14 @@ function ListDetailDataPengangguran() {
{!mounted && !chartData ? ( {!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title> <Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper> </Paper>
</Box> </Box>
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title> <Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
{mounted && chartData.length > 0 && ( {mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}> <Box w={{ base: '100%', md: '70%' }}>
<BarChart <BarChart

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital'; import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -95,27 +96,61 @@ function EditPenghargaan() {
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital'; import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
function CreateDesaDigital() { function CreateDesaDigital() {
@@ -87,25 +88,61 @@ function CreateDesaDigital() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <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>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno'; import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -94,27 +95,61 @@ function EditInfoTeknologiTepatGuna() {
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno'; import infoTeknoState from '../../../_state/inovasi/info-tekno';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() { function CreateInfoTeknologiTepatGuna() {
@@ -87,25 +88,61 @@ function CreateInfoTeknologiTepatGuna() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <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>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -91,57 +92,91 @@ function EditInfoWabahPenyakit() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Info Wabah Penyakit</Title> <Title order={3}>Edit Info Wabah Penyakit</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box>
<FileInput <TextInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} value={formData.deskripsiSingkat}
value={file} onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
onChange={async (e) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
if (!e) return; placeholder="masukkan deskripsi"
setFile(e); />
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz="sm" fw="bold">Deskripsi</Text>
) : ( <EditEditor
<Center w={200} h={200} bg="gray"> value={formData.deskripsi}
<IconImageInPicture /> onChange={(val) => setFormData({ ...formData, deskripsi: val })}
</Center> />
)} </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <div>
Simpan <Text size="xl" inline>
</Button> Drag gambar ke sini atau klik untuk pilih file
</Stack> </Text>
</Paper> <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>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoWabahPenyakit() { function CreateInfoWabahPenyakit() {
const router = useRouter(); const router = useRouter();
@@ -94,28 +95,62 @@ function CreateInfoWabahPenyakit() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -89,49 +90,82 @@ function EditKontakDarurat() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Kontak Darurat</Title> <Title order={3}>Edit Kontak Darurat</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
<Button onClick={handleSubmit} bg={colors['blue-button']}> maxHeight: '200px',
Simpan objectFit: 'contain',
</Button> borderRadius: '8px',
</Stack> border: '1px solid #ddd',
</Paper> }}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,113 +1,147 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import ApiFetch from '@/lib/api-fetch';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { Dropzone } from '@mantine/dropzone';
function CreateKontakDarurat() { function CreateKontakDarurat() {
const router = useRouter(); const router = useRouter();
const kontakDaruratState = useProxy(kontakDarurat) const kontakDaruratState = useProxy(kontakDarurat)
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
kontakDaruratState.create.form = { kontakDaruratState.create.form = {
name: "", name: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
};
setPreviewImage(null);
setFile(null);
}; };
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); 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");
}
kontakDaruratState.create.form.imageId = uploaded.id;
await kontakDaruratState.create.create();
resetForm();
router.push("/admin/kesehatan/kontak-darurat")
} }
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%' }}> const res = await ApiFetch.api.fileStorage.create.post({
<Stack gap="xs"> file,
<Title order={3}>Create Kontak Darurat</Title> name: file.name,
});
<TextInput const uploaded = res.data?.data;
value={kontakDaruratState.create.form.name} if (!uploaded?.id) {
onChange={(val) => { return toast.error("Gagal upload gambar");
kontakDaruratState.create.form.name = val.target.value; }
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box> kontakDaruratState.create.form.imageId = uploaded.id;
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={kontakDaruratState.create.form.deskripsi}
onChange={(val) => {
kontakDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<FileInput await kontakDaruratState.create.create();
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 ? ( resetForm();
<Image alt="" src={previewImage} w={200} h={200} /> router.push("/admin/kesehatan/kontak-darurat")
) : ( }
<Center w={200} h={200} bg="gray"> return (
<IconImageInPicture /> <Box>
</Center> <Box mb={10}>
)} <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Button>
Simpan
</Button>
</Stack>
</Paper>
</Box> </Box>
);
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Kontak Darurat</Title>
<TextInput
value={kontakDaruratState.create.form.name}
onChange={(val) => {
kontakDaruratState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={kontakDaruratState.create.form.deskripsi}
onChange={(val) => {
kontakDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
} }
export default CreateKontakDarurat; export default CreateKontakDarurat;

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -105,28 +106,61 @@ function EditPenangananDarurat() {
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
{/* 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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -79,31 +80,65 @@ function CreatePenangananDarurat() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
Simpan </Box>
</Button> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu'; import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -94,25 +95,62 @@ function EditPosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title> <Title order={4}>Edit Posyandu</Title>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz={"md"} fw={"bold"}>Gambar</Text>
) : ( <Box>
<Center w={200} h={200} bg={"gray"}> <Dropzone
<IconImageInPicture /> onDrop={(files) => {
</Center> const selectedFile = files[0]; // Ambil file pertama
)} if (selectedFile) {
<FileInput setFile(selectedFile);
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
value={file} }
onChange={async (e) => { }}
if (!e) return; onReject={() => toast.error('File tidak valid.')}
setFile(e); maxSize={5 * 1024 ** 2} // Maks 5MB
const base64 = await e.arrayBuffer().then((buf) => accept={{ 'image/*': [] }}
"data:image/png;base64," + Buffer.from(buf).toString("base64") >
); <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
setPreviewImage(base64); <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>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -65,25 +66,62 @@ function CreatePosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={4}>Create Posyandu</Title> <Title order={4}>Create Posyandu</Title>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz={"md"} fw={"bold"}>Gambar</Text>
) : ( <Box>
<Center w={200} h={200} bg={"gray"}> <Dropzone
<IconImageInPicture /> onDrop={(files) => {
</Center> const selectedFile = files[0]; // Ambil file pertama
)} if (selectedFile) {
<FileInput setFile(selectedFile);
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
value={file} }
onChange={async (e) => { }}
if (!e) return; onReject={() => toast.error('File tidak valid.')}
setFile(e); maxSize={5 * 1024 ** 2} // Maks 5MB
const base64 = await e.arrayBuffer().then((buf) => accept={{ 'image/*': [] }}
"data:image/png;base64," + Buffer.from(buf).toString("base64") >
); <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
setPreviewImage(base64); <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>
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>} label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu' placeholder='Masukkan nama posyandu'

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -91,57 +92,90 @@ function EditProgramKesehatan() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Program Kesehatan</Title> <Title order={3}>Edit Program Kesehatan</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box>
<FileInput <TextInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} value={formData.deskripsiSingkat}
value={file} onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
onChange={async (e) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
if (!e) return; placeholder="masukkan deskripsi"
setFile(e); />
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz="sm" fw="bold">Deskripsi</Text>
) : ( <EditEditor
<Center w={200} h={200} bg="gray"> value={formData.deskripsi}
<IconImageInPicture /> onChange={(val) => setFormData({ ...formData, deskripsi: val })}
</Center> />
)} </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <div>
Simpan <Text size="xl" inline>
</Button> Drag gambar ke sini atau klik untuk pilih file
</Stack> </Text>
</Paper> <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>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
import { Dropzone } from '@mantine/dropzone';
function CreateProgramKesehatan() { function CreateProgramKesehatan() {
const router = useRouter(); const router = useRouter();
@@ -94,28 +95,61 @@ function CreateProgramKesehatan() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
{/* 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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -4,8 +4,9 @@
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { ChangeEvent, useEffect, useState } from 'react'; import { ChangeEvent, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -141,22 +142,6 @@ function EditPuskesmas() {
} }
}; };
const handleFileChange = (selectedFile: File | null) => {
if (selectedFile) {
setFile(selectedFile);
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
setPreviewImage(e.target.result as string);
}
};
reader.readAsDataURL(selectedFile);
} else {
setFile(null);
setPreviewImage(null);
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ setFormData(prev => ({
@@ -252,22 +237,62 @@ function EditPuskesmas() {
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/> />
<FileInput <Box>
placeholder="Pilih gambar" <Text fz={"md"} fw={"bold"}>Gambar</Text>
label="Gambar" <Box>
accept="image/*" <Dropzone
leftSection={<IconImageInPicture size={16} />} onDrop={(files) => {
value={file} const selectedFile = files[0]; // Ambil file pertama
onChange={handleFileChange} if (selectedFile) {
/> setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
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>
{previewImage ? ( <div>
<Image alt="Preview" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg="gray"> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -144,7 +145,7 @@ function CreatePuskesmas() {
statePuskesmas.create.form.kontak.facebook = e.target.value; statePuskesmas.create.form.kontak.facebook = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>} label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak ugd" placeholder="masukkan kontak ugd"
value={statePuskesmas.create.form.kontak.kontakUGD} value={statePuskesmas.create.form.kontak.kontakUGD}
@@ -153,27 +154,61 @@ function CreatePuskesmas() {
}} }}
/> />
<FileInput <Box>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
'data:image/png;base64,' + Buffer.from(buf).toString('base64') setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg="gray"> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> 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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Puskesmas Simpan Puskesmas
</Button> </Button>

View File

@@ -36,6 +36,10 @@ function Page() {
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]); const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
useShallowEffect(() => { useShallowEffect(() => {
if (!data && !loading) {
state.findMany.load();
return;
}
if (data) { if (data) {
// Hitung total berdasarkan jenis kelamin // Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length; const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;

View File

@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID'; import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Biodata from './biodata/biodataForm'; import Biodata from './biodata/biodataForm';
@@ -58,21 +59,6 @@ function EditProfilePPID() {
stateProfilePPID.editForm.updateField(field as any, value); stateProfilePPID.editForm.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 () => { const handleSubmit = async () => {
if (isSubmitting || !stateProfilePPID.editForm.form.name.trim()) { if (isSubmitting || !stateProfilePPID.editForm.form.name.trim()) {
toast.error("Nama wajib diisi"); toast.error("Nama wajib diisi");
@@ -183,26 +169,61 @@ function EditProfilePPID() {
/> />
{/* File Upload */} {/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box> <Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
{previewImage ? ( <Box>
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" /> <Dropzone
) : ( onDrop={(files) => {
<Center w={200} h={200} bg="gray.2"> const selectedFile = files[0]; // Ambil file pertama
<Stack align="center" gap="xs"> if (selectedFile) {
<IconImageInPicture size={48} color="gray" /> setFile(selectedFile);
<Text size="sm" c="gray">Tidak ada gambar</Text> setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
</Stack> }
</Center> }}
)} 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> </Box>
{/* Rich Components */} {/* Rich Components */}

View File

@@ -93,12 +93,15 @@ function EditPosisiOrganisasiPPID() {
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>} label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
placeholder='Masukkan nama posisi organisasi' placeholder='Masukkan nama posisi organisasi'
/> />
<EditEditor <Box>
value={formData.deskripsi} <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
onChange={(htmlContent) => { <EditEditor
setFormData({ ...formData, deskripsi: htmlContent }); value={formData.deskripsi}
}} onChange={(htmlContent) => {
/> setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<TextInput <TextInput
value={formData.hierarki} value={formData.hierarki}
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })} onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}

View File

@@ -3,7 +3,7 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -46,12 +46,15 @@ function CreatePosisiOrganisasiPPID() {
value={stateOrganisasi.create.form.nama} value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
/> />
<CreateEditor <Box>
value={stateOrganisasi.create.form.deskripsi} <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
onChange={(htmlContent) => { <CreateEditor
stateOrganisasi.create.form.deskripsi = htmlContent; value={stateOrganisasi.create.form.deskripsi}
}} onChange={(htmlContent) => {
/> stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput <TextInput
label="Hierarki" label="Hierarki"
type="number" type="number"