Compare commits
12 Commits
nico/3-des
...
nico/16-de
| Author | SHA1 | Date | |
|---|---|---|---|
| c8484357cb | |||
| 342e9bbc65 | |||
| f6f77d9e35 | |||
| a00481152c | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| ec3ad12531 | |||
| dad44c0537 |
@@ -3,11 +3,12 @@ module.exports = {
|
|||||||
'postcss-preset-mantine': {},
|
'postcss-preset-mantine': {},
|
||||||
'postcss-simple-vars': {
|
'postcss-simple-vars': {
|
||||||
variables: {
|
variables: {
|
||||||
'mantine-breakpoint-xs': '36em',
|
/* Mobile first */
|
||||||
'mantine-breakpoint-sm': '48em',
|
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||||
'mantine-breakpoint-md': '62em',
|
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||||
'mantine-breakpoint-lg': '75em',
|
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||||
'mantine-breakpoint-xl': '88em',
|
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||||
|
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -828,11 +828,11 @@ model DokterdanTenagaMedis {
|
|||||||
name String
|
name String
|
||||||
specialist String
|
specialist String
|
||||||
jadwal String
|
jadwal String
|
||||||
jadwalLibur String
|
jadwalLibur String?
|
||||||
jamBukaOperasional String
|
jamBukaOperasional String?
|
||||||
jamTutupOperasional String
|
jamTutupOperasional String?
|
||||||
jamBukaLibur String
|
jamBukaLibur String?
|
||||||
jamTutupLibur String
|
jamTutupLibur String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
|
|||||||
BIN
public/mangupuraaward.jpeg
Normal file
BIN
public/mangupuraaward.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -6,14 +6,20 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
nik: z.string().min(3, "NIK minimal 3 karakter"),
|
nik: z
|
||||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
.string()
|
||||||
|
.min(3, "NIK minimal 3 karakter")
|
||||||
|
.max(16, "NIK maksimal 16 angka"),
|
||||||
|
notelp: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||||
|
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||||
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
|
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
|
||||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||||
jenisInformasiDimintaId: z.string().nonempty(),
|
jenisInformasiDimintaId: z.string().nonempty(),
|
||||||
caraMemperolehInformasiId: z.string().nonempty(),
|
caraMemperolehInformasiId: z.string().nonempty(),
|
||||||
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
||||||
})
|
});
|
||||||
|
|
||||||
const jenisInformasiDiminta = proxy({
|
const jenisInformasiDiminta = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
@@ -21,44 +27,58 @@ const jenisInformasiDiminta = proxy({
|
|||||||
| null
|
| null
|
||||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||||
async load() {
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
|
const res =
|
||||||
|
await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const caraMemperolehInformasi = proxy({
|
const caraMemperolehInformasi = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| null
|
| null
|
||||||
| Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.CaraMemperolehInformasiGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}>[],
|
||||||
async load() {
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
|
const res =
|
||||||
|
await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const caraMemperolehSalinanInformasi = proxy({
|
const caraMemperolehSalinanInformasi = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| null
|
| null
|
||||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}>[],
|
||||||
async load() {
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
|
const res =
|
||||||
|
await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
console.log(caraMemperolehSalinanInformasi)
|
console.log(caraMemperolehSalinanInformasi);
|
||||||
|
|
||||||
type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
|
type PermohonanInformasiPublikForm =
|
||||||
|
Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
name: true;
|
name: true;
|
||||||
nik: true;
|
nik: true;
|
||||||
@@ -76,49 +96,61 @@ const statepermohonanInformasiPublik = proxy({
|
|||||||
form: {} as PermohonanInformasiPublikForm,
|
form: {} as PermohonanInformasiPublikForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
|
const cek = templateForm.safeParse(
|
||||||
|
statepermohonanInformasiPublik.create.form
|
||||||
|
);
|
||||||
|
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||||
.map((v) => `${v.path.join(".")}`)
|
return false; // ⬅️ tambahkan return false
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
statepermohonanInformasiPublik.create.loading = true;
|
statepermohonanInformasiPublik.create.loading = true;
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||||
if (res.status === 200) {
|
"create"
|
||||||
statepermohonanInformasiPublik.findMany.load();
|
].post(statepermohonanInformasiPublik.create.form);
|
||||||
return toast.success("Sukses menambahkan");
|
|
||||||
|
if (res.data?.success === false) {
|
||||||
|
toast.error(res.data?.message);
|
||||||
|
return false; // ⬅️ gagal
|
||||||
}
|
}
|
||||||
return toast.error("failed create");
|
|
||||||
} catch (error) {
|
toast.success("Sukses menambahkan");
|
||||||
console.log((error as Error).message);
|
return true; // ⬅️ sukses
|
||||||
|
} catch {
|
||||||
|
toast.error("Terjadi kesalahan server");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
statepermohonanInformasiPublik.create.loading = false;
|
statepermohonanInformasiPublik.create.loading = false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.PermohonanInformasiPublikGetPayload<{ include: {
|
| Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
caraMemperolehSalinanInformasi: true,
|
include: {
|
||||||
jenisInformasiDiminta: true,
|
caraMemperolehSalinanInformasi: true;
|
||||||
caraMemperolehInformasi: true,
|
jenisInformasiDiminta: true;
|
||||||
} }>[]
|
caraMemperolehInformasi: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
|
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
jenisInformasiDiminta: true,
|
jenisInformasiDiminta: true;
|
||||||
caraMemperolehInformasi: true,
|
caraMemperolehInformasi: true;
|
||||||
caraMemperolehSalinanInformasi: true,
|
caraMemperolehSalinanInformasi: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
@@ -137,14 +169,13 @@ const statepermohonanInformasiPublik = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const statepermohonanInformasiPublikForm = proxy({
|
const statepermohonanInformasiPublikForm = proxy({
|
||||||
statepermohonanInformasiPublik,
|
statepermohonanInformasiPublik,
|
||||||
jenisInformasiDiminta,
|
jenisInformasiDiminta,
|
||||||
caraMemperolehInformasi,
|
caraMemperolehInformasi,
|
||||||
caraMemperolehSalinanInformasi,
|
caraMemperolehSalinanInformasi,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default statepermohonanInformasiPublikForm;
|
export default statepermohonanInformasiPublikForm;
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import { z } from "zod";
|
|||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
notelp: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||||
|
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||||
})
|
});
|
||||||
|
|
||||||
type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
type PermohonanKeberatanInformasiForm =
|
||||||
|
Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
name: true;
|
name: true;
|
||||||
email: true;
|
email: true;
|
||||||
@@ -25,23 +29,28 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
form: {} as PermohonanKeberatanInformasiForm,
|
form: {} as PermohonanKeberatanInformasiForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
|
const cek = templateForm.safeParse(
|
||||||
|
permohonanKeberatanInformasi.create.form
|
||||||
|
);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||||
.map((v) => `${v.path.join(".")}`)
|
return false; // ⬅️ tambahkan return false
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
permohonanKeberatanInformasi.create.loading = true;
|
permohonanKeberatanInformasi.create.loading = true;
|
||||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||||
if (res.status === 200) {
|
"create"
|
||||||
permohonanKeberatanInformasi.findMany.load();
|
].post(permohonanKeberatanInformasi.create.form);
|
||||||
return toast.success("Sukses menambahkan");
|
if (res.data?.success === false) {
|
||||||
|
toast.error(res.data?.message);
|
||||||
|
return false; // ⬅️ gagal
|
||||||
}
|
}
|
||||||
return toast.error("failed create");
|
|
||||||
} catch (error) {
|
toast.success("Sukses menambahkan");
|
||||||
console.log((error as Error).message);
|
return true; // ⬅️ sukses
|
||||||
|
} catch {
|
||||||
|
toast.error("Terjadi kesalahan server");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
permohonanKeberatanInformasi.create.loading = false;
|
permohonanKeberatanInformasi.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -49,14 +58,18 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
|
| Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
|
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
@@ -66,12 +79,17 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
const res = await fetch(
|
||||||
|
`/api/ppid/permohonankeberataninformasipublik/${id}`
|
||||||
|
);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
console.error(
|
||||||
|
"Failed to fetch permohonan keberatan informasi:",
|
||||||
|
res.statusText
|
||||||
|
);
|
||||||
permohonanKeberatanInformasi.findUnique.data = null;
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -79,8 +97,7 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
permohonanKeberatanInformasi.findUnique.data = null;
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default permohonanKeberatanInformasi;
|
export default permohonanKeberatanInformasi;
|
||||||
|
|
||||||
|
|||||||
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
|
import stateGallery from "@/app/admin/(dashboard)/_state/desa/gallery";
|
||||||
|
import colors from "@/con/colors";
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
|
import {
|
||||||
|
IconArrowBack,
|
||||||
|
IconPhoto,
|
||||||
|
IconUpload,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
function EditFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imagesId: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imagesId: "",
|
||||||
|
imageUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load kategori + Foto
|
||||||
|
useEffect(() => {
|
||||||
|
FotoState.findMany.load();
|
||||||
|
|
||||||
|
const loadFoto = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await FotoState.update.load(id);
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
name: data.name || "",
|
||||||
|
deskripsi: data.deskripsi || "",
|
||||||
|
imagesId: data.imagesId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setOriginalData({
|
||||||
|
name: data.name || "",
|
||||||
|
deskripsi: data.deskripsi || "",
|
||||||
|
imagesId: data.imagesId || "",
|
||||||
|
imageUrl: data.imageGalleryFoto?.link || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.imageGalleryFoto?.link) {
|
||||||
|
setPreviewImage(data.imageGalleryFoto.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading Foto:", error);
|
||||||
|
toast.error("Gagal memuat data Foto");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFoto();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// Update global state hanya sekali di sini
|
||||||
|
FotoState.update.form = {
|
||||||
|
...FotoState.update.form,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error("Gagal upload gambar");
|
||||||
|
}
|
||||||
|
|
||||||
|
FotoState.update.form.imagesId = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await FotoState.update.update();
|
||||||
|
toast.success("Foto berhasil diperbarui!");
|
||||||
|
router.push("/admin/desa/gallery/foto");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating foto:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat memperbarui foto");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
imagesId: originalData.imagesId,
|
||||||
|
});
|
||||||
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
|
setFile(null);
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Foto
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Judul Foto"
|
||||||
|
placeholder="Masukkan judul foto"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange("name", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Foto
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() =>
|
||||||
|
toast.error("File tidak valid, gunakan format gambar")
|
||||||
|
}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ "image/*": [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload
|
||||||
|
size={48}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 220,
|
||||||
|
objectFit: "contain",
|
||||||
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi Foto
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditFoto;
|
||||||
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Alert } from '@mantine/core';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash, IconPhoto } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
|
|
||||||
|
function DetailFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
FotoState.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
FotoState.delete.byId(selectedId);
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
router.push("/admin/desa/gallery/foto");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!FotoState.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = FotoState.findUnique.data;
|
||||||
|
const imageUrl = data.imageGalleryFoto?.link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
// Gunakan max-width agar tidak terlalu lebar di desktop
|
||||||
|
maw={800}
|
||||||
|
w="100%"
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz={{ base: 'xl', md: '2xl' }} fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Foto
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul Foto</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{imageUrl ? (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '600px', // Set a maximum width
|
||||||
|
margin: '0 auto', // Center the container
|
||||||
|
aspectRatio: '16/9', // Use 16:9 aspect ratio
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageUrl}
|
||||||
|
alt={data.name || 'Gambar Foto'}
|
||||||
|
fill
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain', // Changed from 'cover' to 'contain' to show full image
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : imageError ? (
|
||||||
|
<Alert
|
||||||
|
color="orange"
|
||||||
|
icon={<IconPhoto size={16} />}
|
||||||
|
title="Gagal memuat gambar"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
Gambar tidak dapat ditampilkan.
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Group gap="sm" justify="flex-start">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/foto/${data.id}/edit`)}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah Anda yakin ingin menghapus foto ini?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailFoto;
|
||||||
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
'use client';
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Loader,
|
||||||
|
Image
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreateFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
FotoState.create.form = {
|
||||||
|
name: '',
|
||||||
|
deskripsi: '',
|
||||||
|
imagesId: '',
|
||||||
|
};
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn('Silakan 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 mengunggah gambar, silakan coba lagi');
|
||||||
|
}
|
||||||
|
|
||||||
|
FotoState.create.form.imagesId = uploaded.id;
|
||||||
|
|
||||||
|
await FotoState.create.create();
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/gallery/foto');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating foto:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat membuat foto');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
{/* Header Back Button + Title */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Foto
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Card Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
|
<TextInput
|
||||||
|
label="Judul Foto"
|
||||||
|
placeholder="Masukkan judul Foto"
|
||||||
|
value={FotoState.create.form.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
FotoState.create.form.name = e.currentTarget.value;
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Foto
|
||||||
|
</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={FotoState.create.form.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
FotoState.create.form.deskripsi = val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFoto;
|
||||||
@@ -1,157 +1,163 @@
|
|||||||
"use client";
|
'use client'
|
||||||
import stateFileStorage from "@/state/state-list-image";
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Button,
|
||||||
Flex,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
Title
|
Title
|
||||||
} from "@mantine/core";
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { motion } from "framer-motion";
|
import { useRouter } from 'next/navigation';
|
||||||
import toast from "react-simple-toasts";
|
import { useState } from 'react';
|
||||||
import { useSnapshot } from "valtio";
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
export default function ListImage() {
|
import stateGallery from '../../../_state/desa/gallery';
|
||||||
const { list, total } = useSnapshot(stateFileStorage);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
stateFileStorage.load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let timeOut: NodeJS.Timer;
|
|
||||||
|
|
||||||
|
function Foto() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Stack p="lg" gap="lg">
|
|
||||||
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
|
||||||
<Title order={2} fw={700}>
|
|
||||||
Galeri Foto
|
|
||||||
</Title>
|
|
||||||
<TextInput
|
|
||||||
radius="xl"
|
|
||||||
size="md"
|
|
||||||
placeholder="Cari foto berdasarkan nama..."
|
|
||||||
leftSection={<IconSearch size={18} />}
|
|
||||||
rightSection={
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => stateFileStorage.load()}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (timeOut) clearTimeout(timeOut);
|
|
||||||
timeOut = setTimeout(() => {
|
|
||||||
stateFileStorage.load({ search: e.target.value });
|
|
||||||
}, 300);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Paper withBorder radius="lg" p="md" shadow="sm">
|
|
||||||
{list && list.length > 0 ? (
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
|
||||||
spacing="md"
|
|
||||||
verticalSpacing="md"
|
|
||||||
>
|
|
||||||
{list.map((v, k) => (
|
|
||||||
<Card
|
|
||||||
key={k}
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
className="hover:shadow-md transition-all duration-200"
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<motion.div
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(v.url);
|
|
||||||
toast("Tautan foto berhasil disalin");
|
|
||||||
}}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={`${v.url}?size=200`}
|
|
||||||
alt={v.name}
|
|
||||||
radius="md"
|
|
||||||
h={120}
|
|
||||||
fit="cover"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" fw={500} lineClamp={2}>
|
<HeaderSearch
|
||||||
{v.name}
|
title='Foto'
|
||||||
</Text>
|
placeholder='Cari judul atau deskripsi foto...'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListFoto search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="space-between" align="center" pt="xs">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
radius="md"
|
|
||||||
onClick={() => {
|
|
||||||
stateFileStorage
|
|
||||||
.del({ id: v.id })
|
|
||||||
.finally(() => toast("Foto berhasil dihapus"));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
) : (
|
|
||||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
|
||||||
<Image
|
|
||||||
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
|
||||||
alt="Kosong"
|
|
||||||
w={120}
|
|
||||||
h={120}
|
|
||||||
fit="contain"
|
|
||||||
opacity={0.7}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
Belum ada foto yang tersedia
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{total && total > 1 && (
|
|
||||||
<Flex justify="center">
|
|
||||||
<Pagination
|
|
||||||
total={total}
|
|
||||||
value={stateFileStorage.page} // Changed from page to value
|
|
||||||
size="md"
|
|
||||||
radius="md"
|
|
||||||
withEdges
|
|
||||||
onChange={(page) => {
|
|
||||||
stateFileStorage.load({ page });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ListFoto({ search }: { search: string }) {
|
||||||
|
const FotoState = useProxy(stateGallery.foto)
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = FotoState.findMany;
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
load(page, 10, search)
|
||||||
|
}, [page, search])
|
||||||
|
|
||||||
|
const filteredData = data || []
|
||||||
|
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4}>Daftar Foto</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/gallery/foto/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ width: '25%' }}>Judul Foto</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||||
|
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '30%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada foto yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Foto;
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Profile Desa",
|
label: "Profil Desa",
|
||||||
value: "profiledesa",
|
value: "profildesa",
|
||||||
href: "/admin/desa/profile/profile-desa",
|
href: "/admin/desa/profil/profil-desa",
|
||||||
icon: <IconUser size={18} stroke={1.8} />
|
icon: <IconUser size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel",
|
label: "Profil Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profilperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel",
|
href: "/admin/desa/profil/profil-perbekel",
|
||||||
icon: <IconUsers size={18} stroke={1.8} />
|
icon: <IconUsers size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
label: "Profil Perbekel Dari Masa Ke Masa",
|
||||||
value: "profile-perbekel-dari-masa-ke-masa",
|
value: "profilperbekeldarimasakemasa",
|
||||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
|
||||||
icon: <IconCalendar size={18} stroke={1.8} />
|
icon: <IconCalendar size={18} stroke={1.8} />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Sejarah Desa",
|
label: "Sejarah Desa",
|
||||||
value: "sejarahdesa",
|
value: "sejarahdesa",
|
||||||
href: "/admin/desa/profile/edit/sejarah_desa"
|
href: "/admin/desa/profil/edit/sejarah_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Visi Misi Desa",
|
label: "Visi Misi Desa",
|
||||||
value: "visimisidesa",
|
value: "visimisidesa",
|
||||||
href: "/admin/desa/profile/edit/visi_misi_desa"
|
href: "/admin/desa/profil/edit/visi_misi_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Lambang Desa",
|
label: "Lambang Desa",
|
||||||
value: "lambangdesa",
|
value: "lambangdesa",
|
||||||
href: "/admin/desa/profile/edit/lambang_desa"
|
href: "/admin/desa/profil/edit/lambang_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Maskot Desa",
|
label: "Maskot Desa",
|
||||||
value: "maskotdesa",
|
value: "maskotdesa",
|
||||||
href: "/admin/desa/profile/edit/maskot_desa"
|
href: "/admin/desa/profil/edit/maskot_desa"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ function Page() {
|
|||||||
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
|
<Button onClick={() => router.push('/admin/desa/profil/profil-desa')} variant="outline">
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -40,7 +40,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error("ID tidak valid");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profil/profil-desa");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Maskot berhasil diperbarui!");
|
toast.success("Maskot berhasil diperbarui!");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profil/profil-desa");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update maskot:", error);
|
console.error("Error update maskot:", error);
|
||||||
@@ -50,7 +50,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ function Page() {
|
|||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
@@ -42,7 +42,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ function Page() {
|
|||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
@@ -27,7 +27,7 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
|
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
|
||||||
|
|
||||||
{/* Sejarah Desa */}
|
{/* Sejarah Desa */}
|
||||||
{sejarah && (
|
{sejarah && (
|
||||||
@@ -42,7 +42,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -87,7 +87,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -135,7 +135,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -180,7 +180,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
|
|
||||||
await state.update.update();
|
await state.update.update();
|
||||||
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating perbekel dari masa ke masa:', error);
|
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||||
@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
|
|||||||
state.delete.byId(selectedId);
|
state.delete.byId(selectedId);
|
||||||
setModalHapus(false);
|
setModalHapus(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ function DetailPerbekelDariMasa() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
|
|||||||
state.create.form.imageId = uploaded.id;
|
state.create.form.imageId = uploaded.id;
|
||||||
await state.create.create();
|
await state.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
||||||
@@ -53,7 +53,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
|||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
|
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
@@ -90,7 +90,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
@@ -25,7 +25,7 @@ function ProfilePerbekel() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error("ID tidak valid");
|
||||||
router.push("/admin/desa/profile/profile-perbekel");
|
router.push("/admin/desa/profil/profil-perbekel");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
|
|||||||
const success = await perbekelState.edit.submit()
|
const success = await perbekelState.edit.submit()
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Data berhasil disimpan");
|
toast.success("Data berhasil disimpan");
|
||||||
router.push("/admin/desa/profile/profile-perbekel");
|
router.push("/admin/desa/profil/profil-perbekel");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update sejarah desa:", error);
|
console.error("Error update sejarah desa:", error);
|
||||||
@@ -41,7 +41,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -44,20 +44,58 @@ function CreatePolsekTerdekat() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isValidGoogleMapsEmbed = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
return (
|
||||||
|
u.hostname === 'www.google.com' &&
|
||||||
|
u.pathname === '/maps/embed' &&
|
||||||
|
u.searchParams.has('pb')
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
const { embedMapUrl } = polsekState.create.form;
|
||||||
|
|
||||||
|
// ✅ Validasi Google Maps Embed URL (jika diisi)
|
||||||
|
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
|
||||||
|
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
await polsekState.create.create();
|
await polsekState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/keamanan/polsek-terdekat");
|
router.push("/admin/keamanan/polsek-terdekat");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
toast.error("Gagal menambah polsek terdekat");
|
toast.error("Gagal menambah polsek terdekat");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractEmbedUrl = (input: string): string => {
|
||||||
|
// Jika sudah berupa URL embed yang valid
|
||||||
|
if (input.startsWith('https://www.google.com/maps/embed?')) {
|
||||||
|
return input.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coba parse sebagai HTML string (iframe)
|
||||||
|
const iframeRegex = /<iframe[^>]*src=["']([^"']*)["'][^>]*>/i;
|
||||||
|
const match = input.match(iframeRegex);
|
||||||
|
if (match && match[1]?.startsWith('https://www.google.com/maps/embed?')) {
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak cocok, kembalikan input asli (atau string kosong)
|
||||||
|
return input.trim();
|
||||||
|
};
|
||||||
|
|
||||||
const fetchLayanan = async () => {
|
const fetchLayanan = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/keamanan/layanan-polsek/find-many");
|
const res = await fetch("/api/keamanan/layanan-polsek/find-many");
|
||||||
@@ -190,9 +228,14 @@ function CreatePolsekTerdekat() {
|
|||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={polsekState.create.form.embedMapUrl}
|
value={polsekState.create.form.embedMapUrl}
|
||||||
onChange={(val) => (polsekState.create.form.embedMapUrl = val.target.value)}
|
onChange={(e) => {
|
||||||
|
const rawValue = e.currentTarget.value;
|
||||||
|
const cleanUrl = extractEmbedUrl(rawValue);
|
||||||
|
polsekState.create.form.embedMapUrl = cleanUrl;
|
||||||
|
}}
|
||||||
|
description="Contoh: https://www.google.com/maps/embed?pb=..."
|
||||||
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
|
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
|
||||||
placeholder="Masukkan embed map url"
|
placeholder="Paste iframe dari Google Maps atau URL embed langsung"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={polsekState.create.form.namaTempatMaps}
|
value={polsekState.create.form.namaTempatMaps}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
icon: <IconActivity size={18} stroke={1.8} />
|
icon: <IconActivity size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Grafik Hasil Kepuasan Masyarakat",
|
label: "Penderita Penyakit",
|
||||||
value: "grafikhasilkepuasan",
|
value: "penderitapenyakit",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
|
href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit",
|
||||||
icon: <IconGauge size={18} stroke={1.8} />
|
icon: <IconGauge size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ function EditGrafikHasilKepuasan() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading grafik hasil kepuasan:", err);
|
console.error("Error loading penderita penyakit:", err);
|
||||||
toast.error("Gagal memuat data grafik hasil kepuasan");
|
toast.error("Gagal memuat data penderita penyakit");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,11 +99,11 @@ function EditGrafikHasilKepuasan() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
editState.update.form = { ...editState.update.form, ...formData };
|
editState.update.form = { ...editState.update.form, ...formData };
|
||||||
await editState.update.submit();
|
await editState.update.submit();
|
||||||
toast.success('Grafik hasil kepuasan berhasil diperbarui!');
|
toast.success('penderita penyakit berhasil diperbarui!');
|
||||||
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
|
router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error updating grafik hasil kepuasan:', err);
|
console.error('Error updating penderita penyakit:', err);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
|
toast.error('Terjadi kesalahan saat memperbarui penderita penyakit');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ function EditGrafikHasilKepuasan() {
|
|||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Grafik Hasil Kepuasan
|
Edit Penderita Penyakit
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
state.delete.byId(selectedId);
|
state.delete.byId(selectedId);
|
||||||
setModalHapus(false);
|
setModalHapus(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
Detail Data Grafik Hasil Kepuasan
|
Detail Data Penderita Penyakit
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
@@ -118,7 +118,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
|
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${data.id}/edit`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -40,7 +40,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
await stateGrafikKepuasan.create.create();
|
await stateGrafikKepuasan.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating grafik kepuasan:", error);
|
console.error("Error creating grafik kepuasan:", error);
|
||||||
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
|
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
|
||||||
@@ -62,7 +62,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
|||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Grafik Hasil Kepuasan Masyarakat
|
Tambah Penderita Penyakit
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ function GrafikHasilKepuasanMasyarakat() {
|
|||||||
<Box>
|
<Box>
|
||||||
{/* Header Search */}
|
{/* Header Search */}
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Grafik Hasil Kepuasan Masyarakat'
|
title='Penderita Penyakit'
|
||||||
placeholder='Cari nama atau alamat...'
|
placeholder='Cari nama atau alamat...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -115,14 +115,14 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
{/* Judul + Tombol Tambah */}
|
{/* Judul + Tombol Tambah */}
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
|
<Title order={4}>Daftar Penderita Penyakit</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
|
'/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/create'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -176,7 +176,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
color="blue"
|
color="blue"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
|
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -221,7 +221,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
||||||
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
<Title pb={10} order={4}>Penderita Penyakit</Title>
|
||||||
{mounted && diseaseChartData.length > 0 ? (
|
{mounted && diseaseChartData.length > 0 ? (
|
||||||
<Center>
|
<Center>
|
||||||
<BarChart
|
<BarChart
|
||||||
@@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function DetailSDGSDesa() {
|
|||||||
const data = sdgsState.findUnique.data;
|
const data = sdgsState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -54,7 +54,7 @@ function DetailSDGSDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '60%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function CreateSDGsDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
||||||
|
|
||||||
|
|
||||||
function SdgsDesa() {
|
function SdgsDesa() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +26,7 @@ function SdgsDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListSdgsDesa({ search }: { search: string }) {
|
function ListSdgsDesa({ search }: { search: string }) {
|
||||||
const listState = useProxy(sdgsDesa)
|
const listState = useProxy(sdgsDesa);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -39,10 +38,10 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
} = listState.findMany;
|
} = listState.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
// Handle loading state
|
// Handle loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
@@ -53,12 +52,15 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
const isEmpty = data.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', md: 'lg' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Sdgs Desa</Title>
|
<Title order={2} lh={1.2}>
|
||||||
|
Daftar Sdgs Desa
|
||||||
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color={colors['blue-button']}
|
color={colors['blue-button']}
|
||||||
@@ -68,63 +70,52 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
<TableTh style={{ width: '60%' }}>
|
||||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
<Text fz="sm" fw={600} c="dark.7" ta="left">
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
Nama Sdgs Desa
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" fw={600} c="dark.7" ta="left">
|
||||||
|
Jumlah
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" fw={600} c="dark.7" ta="center">
|
||||||
|
Aksi
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
|
{isEmpty ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={3} style={{ textAlign: 'center', padding: '2rem' }}>
|
<TableTd colSpan={3} ta="center" py="xl">
|
||||||
<Text c="dimmed">Tidak ada data Sdgs Desa</Text>
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data Sdgs Desa
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
filteredData.map((item) => (
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Title order={4}>Daftar Sdgs Desa</Title>
|
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
|
|
||||||
onClick={() => router.push('/admin/landing-page/SDGs/create')}
|
|
||||||
>
|
|
||||||
Tambah Baru
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
|
||||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '60%' }}>
|
<TableTd style={{ width: '60%' }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
<Text fz="md" fw={500} truncate="end" lineClamp={1} lh={1.5}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dark.6" lh={1.5}>
|
||||||
{item.jumlah || '0'}
|
{item.jumlah || '0'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
<TableTd style={{ width: '20%' }} ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -137,12 +128,53 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
{isEmpty ? (
|
||||||
|
<Center py="xl">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5} ta="center">
|
||||||
|
Tidak ada data Sdgs Desa
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="md" radius="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dark.6" lh={1.4}>
|
||||||
|
Jumlah: {item.jumlah || '0'}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/SDGs/${item.id}`)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center mt="lg">
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{!isEmpty && (
|
||||||
|
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
@@ -154,8 +186,9 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SdgsDesa;
|
export default SdgsDesa;
|
||||||
@@ -204,7 +204,7 @@ function EditAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -215,7 +215,7 @@ function EditAPBDes() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function DetailAPBDes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -77,7 +77,7 @@ function DetailAPBDes() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function CreateAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -56,17 +56,21 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar APBDes</Title>
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
|
Daftar APBDes
|
||||||
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -77,29 +81,39 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover miw={0}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>APBDes</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
|
APBDes
|
||||||
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
|
</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Aksi</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
|
Tahun
|
||||||
|
</TableTh>
|
||||||
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
|
Dokumen
|
||||||
|
</TableTh>
|
||||||
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
|
Aksi
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
<Text fw={500} lineClamp={1}>
|
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
|
||||||
APBDes {item.tahun}
|
APBDes {item.tahun}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
<Text fw={500}>{item.tahun || '-'}</Text>
|
<Text fz="md" fw={500} lh={1.5}>
|
||||||
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
{item.file?.link ? (
|
{item.file?.link ? (
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
@@ -110,17 +124,17 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
leftSection={<IconFile size={16} />}
|
leftSection={<IconFile size={16} />}
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
|
fz="sm"
|
||||||
>
|
>
|
||||||
Lihat Dokumen
|
Lihat Dokumen
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Text c="dimmed" fz="sm">
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
Tidak ada dokumen
|
Tidak ada dokumen
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
<Box w={100}>
|
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -128,19 +142,20 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={14} />}
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
fullWidth
|
fz="sm"
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={5}>
|
<TableTd colSpan={4}>
|
||||||
<Center py={20}>
|
<Center py="lg">
|
||||||
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
@@ -149,8 +164,104 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Center mt="md">
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
|
Daftar APBDes
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="md">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper
|
||||||
|
key={item.id}
|
||||||
|
withBorder
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="md"
|
||||||
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
APBDes {item.tahun}
|
||||||
|
</Text>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
|
Tahun
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
|
Dokumen
|
||||||
|
</Text>
|
||||||
|
{item.file?.link ? (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={item.file.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFile size={14} />}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Lihat
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||||
|
Tidak ada
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
|
mt="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Center py="lg">
|
||||||
|
<Text c="dimmed" fz="xs" lh={1.4}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -68,6 +69,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
|
<Box visibleFrom='md' pb={10}>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -90,7 +92,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
flexShrink: 0, // ✅ mencegah tab mengecil
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -98,7 +100,45 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars={false}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TabsList
|
||||||
|
p="xs" // lebih kecil
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "max-content", // ⬅️ kunci
|
||||||
|
maxWidth: "100%", // ⬅️ penting
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
key={i}
|
key={i}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function EditKategoriDesaAntiKorupsi() {
|
|||||||
|
|
||||||
// 🧩 UI
|
// 🧩 UI
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function CreateKategoriDesaAntiKorupsi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -10,9 +27,8 @@ import HeaderSearch from '../../../_com/header';
|
|||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||||
|
|
||||||
|
|
||||||
function KategoriDesaAntiKorupsi() {
|
function KategoriDesaAntiKorupsi() {
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
@@ -28,62 +44,102 @@ function KategoriDesaAntiKorupsi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListKategoriKegiatan({ search }: { search: string }) {
|
function ListKategoriKegiatan({ search }: { search: string }) {
|
||||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = stateKategori.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = stateKategori.findMany;
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateKategori.delete.byId(selectedId)
|
stateKategori.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py="xl">
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Mobile cards
|
||||||
<Box py={10}>
|
const renderMobileCards = () => (
|
||||||
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" mb="md">
|
{filteredData.length > 0 ? (
|
||||||
<Title order={4}>Daftar Kategori Kegiatan</Title>
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="md" withBorder>
|
||||||
|
<Group justify="space-between" align="flex-start">
|
||||||
|
<Box flex={1}>
|
||||||
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45} lineClamp={2}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
|
||||||
color="blue"
|
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
<IconEdit size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
</Group>
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper p="xl" ta="center">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data kategori yang ditemukan
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Desktop table
|
||||||
|
const renderDesktopTable = () => (
|
||||||
|
<Box>
|
||||||
|
<Table highlightOnHover striped verticalSpacing="sm" miw={300}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
<TableTh>Hapus</TableTh>
|
Nama Kategori
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
|
Edit
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
|
Hapus
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
@@ -91,11 +147,11 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Text fw={500} fz="md" lh={1.45} lineClamp={1}>
|
||||||
<Text fw={500} lineClamp={1}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd w={60}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
@@ -105,7 +161,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd w={60}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -122,18 +178,41 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={2}>
|
<TableTd colSpan={3} ta="center" py="xl">
|
||||||
<Center py={20}>
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
|
Tidak ada data kategori yang ditemukan
|
||||||
</Center>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={{ base: 'xl', md: 'xl' }}>
|
||||||
|
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
|
||||||
|
<Title order={2} lh={1.2}>
|
||||||
|
Daftar Kategori Kegiatan
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box visibleFrom="md">{renderDesktopTable()}</Box>
|
||||||
|
<Box hiddenFrom="md">{renderMobileCards()}</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
@@ -141,13 +220,12 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
|
||||||
mb="md"
|
|
||||||
color="blue"
|
color="blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
)}
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
@@ -158,4 +236,4 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriDesaAntiKorupsi
|
export default KategoriDesaAntiKorupsi;
|
||||||
@@ -150,7 +150,7 @@ export default function EditDesaAntiKorupsi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function DetailKegiatanDesa() {
|
|||||||
const data = detailState.findUnique.data;
|
const data = detailState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -53,7 +53,7 @@ export default function DetailKegiatanDesa() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: "100%", md: "50%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function CreateDesaAntiKorupsi() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py="md">
|
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||||
<Skeleton height={650} radius="lg" />
|
<Skeleton height={650} radius="lg" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -46,11 +46,13 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Title order={4}>Data Program Desa Anti Korupsi</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Text c="dimmed" ta="center">
|
Data Program Desa Anti Korupsi
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" ta="center" fz={{ base: 'xs', md: 'sm' }} lh={1.5}>
|
||||||
Belum ada data program yang tersedia
|
Belum ada data program yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -61,48 +63,56 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="md">
|
<Stack gap={'md'}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Program Desa Anti Korupsi</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
|
Daftar Program Desa Anti Korupsi
|
||||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Table
|
<Table
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
|
|
||||||
withRowBorders
|
withRowBorders
|
||||||
verticalSpacing="sm"
|
verticalSpacing="sm"
|
||||||
>
|
>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
|
<TableTh w="50%">Nama Program</TableTh>
|
||||||
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
<TableTh w="30%">Kategori</TableTh>
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh w="20%" ta="center">
|
||||||
|
Aksi
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '50%' }}>
|
<TableTd w="50%">
|
||||||
<Text fw={500} lineClamp={1}>
|
<Text fw={500} lineClamp={1} fz="md" lh={1.5}>
|
||||||
{item.name || '-'}
|
{item.name || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '30%' }}>
|
<TableTd w="30%">
|
||||||
<Box w={200}>
|
<Text fz="sm" c="dimmed" lineClamp={1} lh={1.5}>
|
||||||
<Text fz="sm" c="dimmed" lineClamp={1}>
|
|
||||||
{item.kategori?.name || '-'}
|
{item.kategori?.name || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
<TableTd w="20%" ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -123,7 +133,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={3}>
|
<TableTd colSpan={3}>
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
Tidak ditemukan data dengan kata kunci pencarian
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -132,6 +142,48 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Stack gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
|
||||||
|
{item.name || '-'}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
|
||||||
|
Kategori: {item.kategori?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper p="sm" radius="md" withBorder>
|
||||||
|
<Text ta="center" c="dimmed" fz="xs" lh={1.5}>
|
||||||
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
@@ -144,7 +196,6 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
}}
|
}}
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
mt="md"
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
import { IconChartBar, IconUsers } from '@tabler/icons-react';
|
import { IconChartBar, IconUsers } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -53,7 +53,9 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
|
<Box>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -67,22 +69,25 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
|
|||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsTab
|
<TabsTab
|
||||||
key={i}
|
key={i}
|
||||||
value={e.value}
|
value={tab.value}
|
||||||
leftSection={e.icon}
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{e.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
<TabsPanel key={i} value={e.value}>
|
<TabsPanel key={i} value={e.value}>
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function EditResponden() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function DetailResponden() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -50,7 +50,7 @@ export default function DetailResponden() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py="md">
|
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||||
<Skeleton height={650} radius="lg" />
|
<Skeleton height={650} radius="lg" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -68,11 +68,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Title order={4}>Data Responden</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Text c="dimmed" ta="center">
|
Data Responden
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
Belum ada data responden yang tersedia
|
Belum ada data responden yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -83,12 +85,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="md">
|
<Stack gap={'lg'}>
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||||
<Title order={4} mb="sm">
|
<Title order={2} size="lg" mb="md" lh={1.2}>
|
||||||
Daftar Responden
|
Daftar Responden
|
||||||
</Title>
|
</Title>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table
|
<Table
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
@@ -97,18 +100,18 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
>
|
>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '5%' }}>No</TableTh>
|
<TableTh fz="sm" fw={600} w={60}>No</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Nama</TableTh>
|
<TableTh fz="sm" fw={600}>Nama</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
<TableTh fz="sm" fw={600}>Tanggal</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
|
<TableTh fz="sm" fw={600}>Jenis Kelamin</TableTh>
|
||||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
<TableTh fz="sm" fw={600} w={120}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={5}>
|
<TableTd colSpan={5}>
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
Tidak ditemukan data dengan kata kunci pencarian
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -116,10 +119,9 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
) : (
|
) : (
|
||||||
filteredData.map((item, index) => (
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{index + 1}</TableTd>
|
<TableTd fz="md" lh={1.5}>{index + 1}</TableTd>
|
||||||
<TableTd>{item.name}</TableTd>
|
<TableTd fz="md" lh={1.5}>{item.name}</TableTd>
|
||||||
<TableTd>
|
<TableTd fz="md" lh={1.5}>
|
||||||
<Box w={150}>
|
|
||||||
{item.tanggal
|
{item.tanggal
|
||||||
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -127,13 +129,8 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})
|
})
|
||||||
: '-'}
|
: '-'}
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Box w={100}>
|
|
||||||
{item.jenisKelamin.name}
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
<TableTd fz="md" lh={1.5}>{item.jenisKelamin.name}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -155,8 +152,64 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Title order={2} size="md" lh={1.2} px="md">
|
||||||
|
Daftar Responden
|
||||||
|
</Title>
|
||||||
|
{filteredData.length === 0 ? (
|
||||||
|
<Paper p="md" radius="lg" shadow="sm" mx="md">
|
||||||
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="md" radius="lg" shadow="sm" mx="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Nama</Text>
|
||||||
|
<Text fz="md" lh={1.5}>{item.name}</Text>
|
||||||
|
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Tanggal</Text>
|
||||||
|
<Text fz="md" lh={1.5}>
|
||||||
|
{item.tanggal
|
||||||
|
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Jenis Kelamin</Text>
|
||||||
|
<Text fz="md" lh={1.5}>{item.jenisKelamin.name}</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImac size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mt="xs"
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -167,7 +220,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
}}
|
}}
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
mt="md"
|
mt={{ base: 'md', md: 'lg' }}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -63,6 +64,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
@@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function EditKategoriPrestasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CreateKategoriPrestasi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py="md">
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
@@ -65,28 +65,33 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
{/* DESKTOP: Table */}
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="xl">
|
||||||
<Title order={4} c="dark">List Kategori Prestasi</Title>
|
<Title order={2} size="lg" lh={1.2}>List Kategori Prestasi</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}
|
||||||
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table verticalSpacing="sm" highlightOnHover>
|
<Table verticalSpacing="sm" highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTh><Text fz="sm" fw={600} c="dark">Nama Kategori</Text></TableTh>
|
||||||
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Edit</Text></TableTh>
|
||||||
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Delete</Text></TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={3} ta="center">
|
||||||
<Text py="md" c="dimmed">
|
<Text py="md" c="dimmed" fz="sm" lh={1.5}>
|
||||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -95,21 +100,21 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Text truncate="end" fz="md" lh={1.5} c="dark">
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -119,7 +124,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
@@ -127,10 +132,9 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Center mt="lg">
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => load(newPage)}
|
||||||
@@ -147,7 +151,69 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* MOBILE: Card */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
{filteredData.length === 0 ? (
|
||||||
|
<Paper p="lg" ta="center">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
) : (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="md" withBorder bg={colors['white-1']}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fz="sm" lh={1.5} fw={600} c="dark">{item.name}</Text>
|
||||||
|
<Group justify="flex-end" gap="xs">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconEdit size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={14} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center py="lg">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
control: {
|
||||||
|
'&[data-active]': {
|
||||||
|
background: `${colors['blue-button']} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
@@ -155,6 +221,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||||
/>
|
/>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export default function EditPrestasiDesa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function DetailPrestasiDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -53,7 +53,7 @@ function DetailPrestasiDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function CreatePrestasiDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -28,6 +28,7 @@ function ListPrestasiDesa() {
|
|||||||
function ListPrestasi({ search }: { search: string }) {
|
function ListPrestasi({ search }: { search: string }) {
|
||||||
const listState = useProxy(prestasiState.prestasiDesa)
|
const listState = useProxy(prestasiState.prestasiDesa)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,60 +40,65 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, []);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Prestasi Desa</Title>
|
<Title order={2} size={isMobile ? 'md' : 'lg'} lh={1.2}>
|
||||||
|
Daftar Prestasi Desa
|
||||||
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
||||||
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover>
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
|
<Table highlightOnHover miw={0}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
|
<TableTh w="25%">Nama Prestasi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
<TableTh w="25%">Deskripsi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
<TableTh w="25%">Kategori</TableTh>
|
||||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh w="25%" ta="center">Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Box w={100}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Box w={150}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
|
{item.kategori?.name || 'Tidak ada kategori'}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%', textAlign: 'center' }}>
|
<TableTd w="25%" ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -108,23 +114,63 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={4} ta="center">
|
||||||
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
|
<Text c="dimmed" py="md" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||||
|
Kategori: {item.kategori?.name || 'Tidak ada kategori'}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py="md">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Center mt="lg">
|
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={load}
|
onChange={load}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
withEdges
|
withEdges
|
||||||
size="sm"
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -74,6 +75,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
|
<Box visibleFrom='md' pb={10}>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -104,6 +106,45 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars={false}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TabsList
|
||||||
|
p="xs" // lebih kecil
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "max-content", // ⬅️ kunci
|
||||||
|
maxWidth: "100%", // ⬅️ penting
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function EditMediaSosial() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function DetailMediaSosial() {
|
|||||||
const data = stateMediaSosial.findUnique.data;
|
const data = stateMediaSosial.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -62,7 +62,7 @@ function DetailMediaSosial() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "50%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||||
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
||||||
|
|
||||||
|
|
||||||
// ⭐ Tambah type SosmedKey
|
// ⭐ Tambah type SosmedKey
|
||||||
type SosmedKey =
|
type SosmedKey =
|
||||||
| 'facebook'
|
| 'facebook'
|
||||||
@@ -88,7 +87,6 @@ export default function CreateMediaSosial() {
|
|||||||
stateMediaSosial.create.form.imageId = null;
|
stateMediaSosial.create.form.imageId = null;
|
||||||
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
||||||
|
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
await stateMediaSosial.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
router.push('/admin/landing-page/profil/media-sosial');
|
||||||
@@ -129,13 +127,13 @@ export default function CreateMediaSosial() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={2} ml="sm" c="dark" lh={1.2} fz={{ base: 'md', md: 'lg' }}>
|
||||||
Tambah Media Sosial
|
Tambah Media Sosial
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -155,7 +153,7 @@ export default function CreateMediaSosial() {
|
|||||||
{/* Custom icon uploader */}
|
{/* Custom icon uploader */}
|
||||||
{selectedSosmed === 'custom' && (
|
{selectedSosmed === 'custom' && (
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz={{ base: 'sm', md: 'md' }} lh={1.45} mb={6}>
|
||||||
Upload Custom Icon
|
Upload Custom Icon
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -185,8 +183,10 @@ export default function CreateMediaSosial() {
|
|||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
<Text size="sm" c="dimmed">
|
Seret gambar atau klik untuk pilih
|
||||||
|
</Text>
|
||||||
|
<Text fz={{ base: 12, md: 'sm' }} c="dimmed" lh={1.4}>
|
||||||
Maksimal 5MB, format .png, .jpg, .jpeg, webp
|
Maksimal 5MB, format .png, .jpg, .jpeg, webp
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -229,7 +229,11 @@ export default function CreateMediaSosial() {
|
|||||||
|
|
||||||
{/* Input name */}
|
{/* Input name */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Media Sosial"
|
label={
|
||||||
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Nama Media Sosial
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
placeholder="Masukkan nama media sosial"
|
placeholder="Masukkan nama media sosial"
|
||||||
value={stateMediaSosial.create.form.name ?? ''}
|
value={stateMediaSosial.create.form.name ?? ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
||||||
@@ -238,7 +242,11 @@ export default function CreateMediaSosial() {
|
|||||||
|
|
||||||
{/* Input link */}
|
{/* Input link */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link / Kontak"
|
label={
|
||||||
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Link / Kontak
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
placeholder="Masukkan link atau nomor"
|
placeholder="Masukkan link atau nomor"
|
||||||
value={stateMediaSosial.create.form.iconUrl ?? ''}
|
value={stateMediaSosial.create.form.iconUrl ?? ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
/* 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 { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -28,7 +46,7 @@ function MediaSosial() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListMediaSosial({ search }: { search: string }) {
|
function ListMediaSosial({ search }: { search: string }) {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
const getIconSource = (item: any) => {
|
||||||
@@ -48,70 +66,95 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
} = stateMediaSosial.findMany;
|
} = stateMediaSosial.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'sm', sm: 'md' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', sm: 'md' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', sm: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', sm: 'md' }}>
|
||||||
<Title order={4}>Daftar Media Sosial</Title>
|
<Title order={4} lh={1.15}>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}>
|
Daftar Media Sosial
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}
|
||||||
|
fz={{ base: 'xs', sm: 'sm' }}
|
||||||
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
|
<Box>
|
||||||
|
{/* Desktop: Table | Mobile: Card-based vertical layout */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>Nama Media Sosial / Kontak</TableTh>
|
<TableTh style={{ width: '25%' }}>
|
||||||
<TableTh style={{ width: '20%' }}>Gambar</TableTh>
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
<TableTh style={{ width: '20%' }}>Link / No. Telepon</TableTh>
|
Nama Media Sosial / Kontak
|
||||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
|
Gambar
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
|
Link / No. Telepon
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>
|
||||||
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
|
Aksi
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%', }}>
|
<TableTd style={{ width: '25%' }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
<Text fw={500} fz="md" lh={1.5} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
|
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const src = getIconSource(item);
|
const src = getIconSource(item);
|
||||||
|
|
||||||
if (src) {
|
if (src) {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={src}
|
src={src}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
fit={item.image?.link ? "cover" : "contain"}
|
fit={item.image?.link ? 'cover' : 'contain'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Box w={250}>
|
<Box w={250}>
|
||||||
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
|
<Text truncate fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
|
||||||
{item.iconUrl || item.noTelp || '-'}
|
{item.iconUrl || item.noTelp || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -123,7 +166,9 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
@@ -134,7 +179,9 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTd colSpan={4}>
|
||||||
<Center py={20}>
|
<Center py={20}>
|
||||||
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
|
<Text c="dimmed" fz="md" lh={1.5}>
|
||||||
|
Tidak ada data media sosial yang cocok
|
||||||
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
@@ -142,7 +189,78 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile layout */}
|
||||||
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||||
|
<Group justify="space-between" wrap="nowrap" align='center'>
|
||||||
|
<Box>
|
||||||
|
<Text fw={600} fz="sm" lh={1.45}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
|
||||||
|
{(() => {
|
||||||
|
const src = getIconSource(item);
|
||||||
|
if (src) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src={src}
|
||||||
|
alt={item.name}
|
||||||
|
fit={item.image?.link ? 'cover' : 'contain'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||||
|
})()}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Box>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c="blue"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{item.iconUrl || item.noTelp || '-'}
|
||||||
|
</Text>
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
<Group mt="sm" justify="flex-end">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py={24}>
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data media sosial yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ function EditPejabatDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
@@ -36,9 +35,9 @@ function Page() {
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
|
style={{fontSize: 15, fontWeight: "bold"}}
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
|
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
|
||||||
>
|
>
|
||||||
@@ -93,7 +92,7 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
<Box mt="lg">
|
<Box mt="lg">
|
||||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
|
||||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="left" c={colors['blue-button']}>
|
||||||
{item.position}
|
{item.position}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function EditProgramInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ function DetailProgramInovasi() {
|
|||||||
const data = stateProgramInovasi.findUnique.data
|
const data = stateProgramInovasi.findUnique.data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'md', md: 'xl' }} py="lg">
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
|
<Box pb="20">
|
||||||
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
|
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function CreateProgramInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function ProgramInovasi() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px="md" py="lg">
|
<Box px={{base: 0, md: "md"}} py="lg">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title="Program Inovasi"
|
title="Program Inovasi"
|
||||||
placeholder="Cari program inovasi..."
|
placeholder="Cari program inovasi..."
|
||||||
@@ -61,6 +61,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
Tambah Program
|
Tambah Program
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Box visibleFrom='md'>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
@@ -121,6 +122,67 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box hiddenFrom="md" pt={20}>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<Paper
|
||||||
|
key={item.id}
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
shadow="xs"
|
||||||
|
>
|
||||||
|
<Stack gap={6}>
|
||||||
|
{/* Title */}
|
||||||
|
<Text fw={600}>{item.name}</Text>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text fz="sm" c="gray.7" lineClamp={2}>
|
||||||
|
{item.description || '-'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
|
<Box>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c="blue"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{item.link}
|
||||||
|
</Text>
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/landing-page/profil/program-inovasi/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{filteredData.length > 0 && (
|
{filteredData.length > 0 && (
|
||||||
<Center mt="md">
|
<Center mt="md">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Grid align="center">
|
<Grid>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
|
w={{base: '100%', md: "110%"}}
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ export const devBar = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "Desa_1",
|
id: "Desa_1",
|
||||||
name: "Profile",
|
name: "Profil",
|
||||||
path: "/admin/desa/profile/profile-desa"
|
path: "/admin/desa/profil/profil-desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Desa_2",
|
id: "Desa_2",
|
||||||
@@ -495,8 +495,8 @@ export const navBar = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "Desa_1",
|
id: "Desa_1",
|
||||||
name: "Profile",
|
name: "Profil",
|
||||||
path: "/admin/desa/profile/profile-desa"
|
path: "/admin/desa/profil/profil-desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Desa_2",
|
id: "Desa_2",
|
||||||
@@ -899,8 +899,8 @@ export const role1 = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "Desa_1",
|
id: "Desa_1",
|
||||||
name: "Profile",
|
name: "Profil",
|
||||||
path: "/admin/desa/profile/profile-desa"
|
path: "/admin/desa/profil/profil-desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Desa_2",
|
id: "Desa_2",
|
||||||
|
|||||||
@@ -1,399 +1,3 @@
|
|||||||
// 'use client'
|
|
||||||
|
|
||||||
// import colors from "@/con/colors";
|
|
||||||
// import { authStore } from "@/store/authStore";
|
|
||||||
// import {
|
|
||||||
// ActionIcon,
|
|
||||||
// AppShell,
|
|
||||||
// AppShellHeader,
|
|
||||||
// AppShellMain,
|
|
||||||
// AppShellNavbar,
|
|
||||||
// Burger,
|
|
||||||
// Center,
|
|
||||||
// Flex,
|
|
||||||
// Group,
|
|
||||||
// Image,
|
|
||||||
// Loader,
|
|
||||||
// NavLink,
|
|
||||||
// ScrollArea,
|
|
||||||
// Text,
|
|
||||||
// Tooltip,
|
|
||||||
// rem
|
|
||||||
// } from "@mantine/core";
|
|
||||||
// import { useDisclosure } from "@mantine/hooks";
|
|
||||||
// import {
|
|
||||||
// IconChevronLeft,
|
|
||||||
// IconChevronRight,
|
|
||||||
// IconLogout2
|
|
||||||
// } from "@tabler/icons-react";
|
|
||||||
// import _ from "lodash";
|
|
||||||
// import Link from "next/link";
|
|
||||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
|
||||||
// import { useEffect, useState } from "react";
|
|
||||||
// // import { useSnapshot } from "valtio";
|
|
||||||
// import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
|
||||||
|
|
||||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
|
||||||
// const [opened, { toggle }] = useDisclosure();
|
|
||||||
// const [loading, setLoading] = useState(true);
|
|
||||||
// const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
||||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
|
||||||
// const router = useRouter();
|
|
||||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
|
||||||
|
|
||||||
// // const { user } = useSnapshot(authStore);
|
|
||||||
|
|
||||||
// // console.log("Current user in store:", user);
|
|
||||||
|
|
||||||
// // ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchUser = async () => {
|
|
||||||
// try {
|
|
||||||
// const res = await fetch('/api/auth/me');
|
|
||||||
// const data = await res.json();
|
|
||||||
|
|
||||||
// if (data.user) {
|
|
||||||
// // ✅ Check if user is NOT active → redirect to waiting room
|
|
||||||
// if (!data.user.isActive) {
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/waiting-room');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ✅ Fetch menuIds
|
|
||||||
// const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
|
||||||
// const menuData = await menuRes.json();
|
|
||||||
|
|
||||||
// const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
|
||||||
// ? [...menuData.menuIds]
|
|
||||||
// : null;
|
|
||||||
|
|
||||||
// // ✅ Set user dengan menuIds yang fresh
|
|
||||||
// authStore.setUser({
|
|
||||||
// id: data.user.id,
|
|
||||||
// name: data.user.name,
|
|
||||||
// roleId: Number(data.user.roleId),
|
|
||||||
// menuIds,
|
|
||||||
// isActive: data.user.isActive
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // ✅ TAMBAHKAN INI: Redirect ke dashboard sesuai roleId
|
|
||||||
// const currentPath = window.location.pathname;
|
|
||||||
// const expectedPath = getRedirectPath(Number(data.user.roleId));
|
|
||||||
|
|
||||||
// // Jika user di halaman /admin tapi bukan di path yang sesuai roleId
|
|
||||||
// if (currentPath === '/admin' || !currentPath.startsWith(expectedPath)) {
|
|
||||||
// router.replace(expectedPath);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// } else {
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/login');
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Gagal memuat data pengguna:', error);
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/login');
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetchUser();
|
|
||||||
// }, [router]);
|
|
||||||
|
|
||||||
// // ✅ Fungsi helper untuk get redirect path
|
|
||||||
// const getRedirectPath = (roleId: number): string => {
|
|
||||||
// switch (roleId) {
|
|
||||||
// case 0: // DEVELOPER
|
|
||||||
// case 1: // SUPERADMIN
|
|
||||||
// case 2: // ADMIN_DESA
|
|
||||||
// return '/admin/landing-page/profil/program-inovasi';
|
|
||||||
// case 3: // ADMIN_KESEHATAN
|
|
||||||
// return '/admin/kesehatan/posyandu';
|
|
||||||
// case 4: // ADMIN_PENDIDIKAN
|
|
||||||
// return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
|
||||||
// default:
|
|
||||||
// return '/admin';
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (loading) {
|
|
||||||
// return (
|
|
||||||
// <AppShell>
|
|
||||||
// <AppShellMain>
|
|
||||||
// <Center h="100vh">
|
|
||||||
// <Loader />
|
|
||||||
// </Center>
|
|
||||||
// </AppShellMain>
|
|
||||||
// </AppShell>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ✅ Ambil menu berdasarkan roleId dan menuIds
|
|
||||||
// const currentNav = authStore.user
|
|
||||||
// ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
|
||||||
// : [];
|
|
||||||
|
|
||||||
// const handleLogout = async () => {
|
|
||||||
// try {
|
|
||||||
// setIsLoggingOut(true);
|
|
||||||
|
|
||||||
// // ✅ Panggil API logout untuk clear session di server
|
|
||||||
// const response = await fetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
// const result = await response.json();
|
|
||||||
|
|
||||||
// if (result.success) {
|
|
||||||
// // Clear user data dari store
|
|
||||||
// authStore.setUser(null);
|
|
||||||
|
|
||||||
// // Clear localStorage
|
|
||||||
// localStorage.removeItem('auth_nomor');
|
|
||||||
// localStorage.removeItem('auth_kodeId');
|
|
||||||
|
|
||||||
// // Force reload untuk reset semua state
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// } else {
|
|
||||||
// console.error('Logout failed:', result.message);
|
|
||||||
// // Tetap redirect meskipun gagal
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error during logout:', error);
|
|
||||||
// // Tetap clear store dan redirect jika error
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// } finally {
|
|
||||||
// setIsLoggingOut(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <AppShell
|
|
||||||
// suppressHydrationWarning
|
|
||||||
// header={{ height: 64 }}
|
|
||||||
// navbar={{
|
|
||||||
// width: { base: 260, sm: 280, lg: 300 },
|
|
||||||
// breakpoint: 'sm',
|
|
||||||
// collapsed: {
|
|
||||||
// mobile: !opened,
|
|
||||||
// desktop: !desktopOpened,
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// padding="md"
|
|
||||||
// >
|
|
||||||
// <AppShellHeader
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
|
||||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// padding: '0 16px',
|
|
||||||
// }}
|
|
||||||
// px={{ base: 'sm', sm: 'md' }}
|
|
||||||
// py={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
|
||||||
// <Flex align="center" gap="sm">
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={{ base: 32, sm: 40 }}
|
|
||||||
// h={{ base: 32, sm: 40 }}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '32px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <Text
|
|
||||||
// fw={700}
|
|
||||||
// c={colors["blue-button"]}
|
|
||||||
// fz={{ base: 'md', sm: 'xl' }}
|
|
||||||
// >
|
|
||||||
// Admin Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// </Flex>
|
|
||||||
|
|
||||||
// <Group gap="xs">
|
|
||||||
// {!desktopOpened && (
|
|
||||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronRight />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <Burger
|
|
||||||
// opened={opened}
|
|
||||||
// onClick={toggle}
|
|
||||||
// hiddenFrom="sm"
|
|
||||||
// size="md"
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// mr="xs"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={() => {
|
|
||||||
// router.push("/darmasaba");
|
|
||||||
// }}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// >
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={20}
|
|
||||||
// h={20}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '20px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={handleLogout}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// loading={isLoggingOut}
|
|
||||||
// disabled={isLoggingOut}
|
|
||||||
// >
|
|
||||||
// <IconLogout2 size={22} />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </Group>
|
|
||||||
// </AppShellHeader>
|
|
||||||
|
|
||||||
// <AppShellNavbar
|
|
||||||
// component={ScrollArea}
|
|
||||||
// style={{
|
|
||||||
// background: "#ffffff",
|
|
||||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// }}
|
|
||||||
// p={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <AppShell.Section p="sm">
|
|
||||||
// {currentNav.map((v, k) => {
|
|
||||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={k}
|
|
||||||
// defaultOpened={isParentActive}
|
|
||||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
|
||||||
// {v.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// style={{
|
|
||||||
// borderRadius: rem(10),
|
|
||||||
// marginBottom: rem(4),
|
|
||||||
// transition: "background 150ms ease",
|
|
||||||
// }}
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// variant="light"
|
|
||||||
// active={isParentActive}
|
|
||||||
// >
|
|
||||||
// {v.children.map((child, key) => {
|
|
||||||
// const isChildActive = segments.includes(
|
|
||||||
// _.lowerCase(child.name)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={key}
|
|
||||||
// href={child.path}
|
|
||||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
|
||||||
// {child.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// borderRadius: rem(8),
|
|
||||||
// marginBottom: rem(2),
|
|
||||||
// transition: 'background 150ms ease',
|
|
||||||
// padding: '6px 12px',
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// ...(isChildActive && {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// active={isChildActive}
|
|
||||||
// component={Link}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </NavLink>
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </AppShell.Section>
|
|
||||||
|
|
||||||
// <AppShell.Section py="md">
|
|
||||||
// <Group justify="end" pr="sm">
|
|
||||||
// <Tooltip
|
|
||||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
|
||||||
// position="top"
|
|
||||||
// withArrow
|
|
||||||
// >
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronLeft />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </AppShell.Section>
|
|
||||||
// </AppShellNavbar>
|
|
||||||
|
|
||||||
// <AppShellMain
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
|
||||||
// minHeight: "100vh",
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {children}
|
|
||||||
// </AppShellMain>
|
|
||||||
// </AppShell>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// app/admin/layout.tsx
|
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
@@ -429,7 +33,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close'
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
@@ -441,21 +45,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/auth/me', {
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
// ✅ Check if user is NOT active → redirect to waiting room
|
|
||||||
if (!data.user.isActive) {
|
if (!data.user.isActive) {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/waiting-room');
|
router.replace('/waiting-room');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fetch menuIds
|
|
||||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const menuData = await menuRes.json();
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
@@ -463,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
? [...menuData.menuIds]
|
? [...menuData.menuIds]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ✅ Set user dengan menuIds yang fresh
|
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
@@ -472,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
isActive: data.user.isActive
|
isActive: data.user.isActive
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
if (currentPath === '/admin') {
|
if (currentPath === '/admin') {
|
||||||
@@ -480,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||||
router.replace(expectedPath);
|
router.replace(expectedPath);
|
||||||
}
|
}
|
||||||
// ✅ Jangan redirect jika user sudah di path yang valid
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
@@ -496,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [router]); // ✅ Only depend on router
|
}, [router]);
|
||||||
|
|
||||||
const getRedirectPath = (roleId: number): string => {
|
const getRedirectPath = (roleId: number): string => {
|
||||||
switch (roleId) {
|
switch (roleId) {
|
||||||
case 0: // DEVELOPER
|
case 0:
|
||||||
case 1: // SUPERADMIN
|
case 1:
|
||||||
case 2: // ADMIN_DESA
|
case 2:
|
||||||
return '/admin/landing-page/profil/program-inovasi';
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
case 3: // ADMIN_KESEHATAN
|
case 3:
|
||||||
return '/admin/kesehatan/posyandu';
|
return '/admin/kesehatan/posyandu';
|
||||||
case 4: // ADMIN_PENDIDIKAN
|
case 4:
|
||||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
default:
|
default:
|
||||||
return '/admin';
|
return '/admin';
|
||||||
@@ -535,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const response = await fetch('/api/auth/logout', {
|
const response = await fetch('/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -559,6 +158,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Handler untuk menutup mobile menu saat navigasi
|
||||||
|
const handleNavClick = (path: string) => {
|
||||||
|
router.push(path);
|
||||||
|
close(); // Tutup mobile menu
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
@@ -573,7 +178,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||||
@@ -626,16 +230,48 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
||||||
{/* ... Navbar content sama seperti sebelumnya ... */}
|
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{currentNav.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
return (
|
return (
|
||||||
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
<NavLink
|
||||||
|
key={k}
|
||||||
|
defaultOpened={isParentActive}
|
||||||
|
c={isParentActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>}
|
||||||
|
style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
|
||||||
|
styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
|
||||||
|
variant="light"
|
||||||
|
active={isParentActive}
|
||||||
|
>
|
||||||
{v.children.map((child, key) => {
|
{v.children.map((child, key) => {
|
||||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||||
return (
|
return (
|
||||||
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
<NavLink
|
||||||
|
key={key}
|
||||||
|
// ✅ PERBAIKAN: Gunakan onClick untuk handle navigasi dan close menu
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNavClick(child.path);
|
||||||
|
}}
|
||||||
|
href={child.path}
|
||||||
|
c={isChildActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: rem(8),
|
||||||
|
marginBottom: rem(2),
|
||||||
|
transition: 'background 150ms ease',
|
||||||
|
padding: '6px 12px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
|
||||||
|
},
|
||||||
|
...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
active={isChildActive}
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -6,33 +6,24 @@ import path from "path";
|
|||||||
const beritaDelete = async (context: Context) => {
|
const beritaDelete = async (context: Context) => {
|
||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) return { status: 400, body: "ID tidak diberikan" };
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: "ID tidak diberikan",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const berita = await prisma.berita.findUnique({
|
const berita = await prisma.berita.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: { image: true, kategoriBerita: true },
|
||||||
image: true,
|
|
||||||
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!berita) {
|
if (!berita) return { status: 404, body: "Berita tidak ditemukan" };
|
||||||
return {
|
|
||||||
status: 404,
|
|
||||||
body: "Berita tidak ditemukan",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hapus file gambar dari filesystem jika ada
|
// 1. HAPUS BERITA DULU
|
||||||
|
await prisma.berita.delete({ where: { id } });
|
||||||
|
|
||||||
|
// 2. BARU HAPUS FILE
|
||||||
if (berita.image) {
|
if (berita.image) {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(berita.image.path, berita.image.name);
|
const filePath = path.join(berita.image.path, berita.image.name);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
|
|
||||||
await prisma.fileStorage.delete({
|
await prisma.fileStorage.delete({
|
||||||
where: { id: berita.image.id },
|
where: { id: berita.image.id },
|
||||||
});
|
});
|
||||||
@@ -41,15 +32,11 @@ const beritaDelete = async (context: Context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hapus berita dari DB
|
|
||||||
await prisma.berita.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berita dan file terkait berhasil dihapus",
|
message: "Berita dan file terkait berhasil dihapus",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default beritaDelete;
|
export default beritaDelete;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default async function grafikJumlahPendudukMiskinFindMany(
|
|||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { year: "asc" },
|
||||||
}),
|
}),
|
||||||
prisma.grafikJumlahPendudukMiskin.count({
|
prisma.grafikJumlahPendudukMiskin.count({
|
||||||
where,
|
where,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Elysia from "elysia";
|
import Elysia from "elysia";
|
||||||
import DaftarInformasiPublik from "./daftar_informasi_publik";
|
import DaftarInformasiPublik from "./daftar_informasi_publik";
|
||||||
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
|
|
||||||
import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin";
|
import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin";
|
||||||
import GrafikBerdasarkanResponden from "./ikm/grafik_responden";
|
import GrafikBerdasarkanResponden from "./ikm/grafik_responden";
|
||||||
import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur";
|
import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur";
|
||||||
@@ -10,6 +9,7 @@ import ProfilePPID from "./profile_ppid";
|
|||||||
import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid";
|
import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid";
|
||||||
import DasarHukumPPID from "./dasar_hukum";
|
import DasarHukumPPID from "./dasar_hukum";
|
||||||
import StrukturPPID from "./struktur_ppid";
|
import StrukturPPID from "./struktur_ppid";
|
||||||
|
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,30 @@ type FormCreate = Prisma.PermohonanInformasiPublikGetPayload<{
|
|||||||
jenisInformasiDimintaId: true;
|
jenisInformasiDimintaId: true;
|
||||||
caraMemperolehInformasiId: true;
|
caraMemperolehInformasiId: true;
|
||||||
caraMemperolehSalinanInformasiId: true;
|
caraMemperolehSalinanInformasiId: true;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export default async function permohonanInformasiPublikCreate(context: Context) {
|
export default async function permohonanInformasiPublikCreate(context: Context) {
|
||||||
const body = context.body as FormCreate;
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
|
// ========== VALIDASI NIK ==========
|
||||||
|
if (body.nik && body.nik.length > 16) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 400,
|
||||||
|
message: "Maksimal NIK adalah 16 angka",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== VALIDASI NOMOR TELEPON ==========
|
||||||
|
if (body.notelp && body.notelp.length > 15) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 400,
|
||||||
|
message: "Maksimal nomor telepon adalah 15 angka",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.permohonanInformasiPublik.create({
|
await prisma.permohonanInformasiPublik.create({
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
@@ -27,15 +46,12 @@ export default async function permohonanInformasiPublikCreate(context: Context)
|
|||||||
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
|
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
|
||||||
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
|
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
|
||||||
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
|
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Permohonan Informasi Publik Berhasil Dibuat",
|
message: "Permohonan Informasi Publik Berhasil Dibuat",
|
||||||
data: {
|
data: { ...body },
|
||||||
...body,
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -8,26 +8,37 @@ type FormCreate = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
|||||||
email: true;
|
email: true;
|
||||||
notelp: true;
|
notelp: true;
|
||||||
alasan: true;
|
alasan: true;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export default async function permohonanKeberatanInformasiPublikCreate(context: Context) {
|
export default async function permohonanKeberatanInformasiPublikCreate(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
const body = context.body as FormCreate;
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
|
// ========== VALIDASI NOMOR TELEPON ==========
|
||||||
|
if (body.notelp && body.notelp.length > 15) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 400,
|
||||||
|
message: "Maksimal nomor telepon adalah 15 angka",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.formulirPermohonanKeberatan.create({
|
await prisma.formulirPermohonanKeberatan.create({
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
email: body.email,
|
email: body.email,
|
||||||
notelp: body.notelp,
|
notelp: body.notelp,
|
||||||
alasan: body.alasan,
|
alasan: body.alasan,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
|
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
43
src/app/api/news/latest/route.ts
Normal file
43
src/app/api/news/latest/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// app/api/news/latest/route.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const berita = await prisma.berita.findMany({
|
||||||
|
take: 3,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: { kategoriBerita: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const pengumuman = await prisma.pengumuman.findMany({
|
||||||
|
take: 3,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: { CategoryPengumuman: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const news = [
|
||||||
|
...berita.map((b) => ({
|
||||||
|
id: b.id,
|
||||||
|
type: "berita" as const,
|
||||||
|
title: b.judul,
|
||||||
|
content: b.content,
|
||||||
|
timestamp: b.createdAt,
|
||||||
|
kategoriBerita: b.kategoriBerita || undefined,
|
||||||
|
})),
|
||||||
|
...pengumuman.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
type: "pengumuman" as const,
|
||||||
|
title: p.judul,
|
||||||
|
content: p.content,
|
||||||
|
timestamp: p.createdAt,
|
||||||
|
kategoriPengumuman: p.CategoryPengumuman || undefined,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, news }); // ✅ ganti 'data' jadi 'news'
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API Error:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "Gagal memuat data" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -51,10 +51,14 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||||
{/* === Berita Utama === */}
|
{/* === Berita Utama === */}
|
||||||
{featuredState.loading ? (
|
{featuredState.loading ? (
|
||||||
<Center><Skeleton h={400} /></Center>
|
<Center>
|
||||||
|
<Skeleton h={400} />
|
||||||
|
</Center>
|
||||||
) : featured ? (
|
) : featured ? (
|
||||||
<Box mb={50}>
|
<Box mb={50}>
|
||||||
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
|
<Title order={2} mb="md">
|
||||||
|
Berita Utama
|
||||||
|
</Title>
|
||||||
<Paper shadow="md" radius="md" withBorder>
|
<Paper shadow="md" radius="md" withBorder>
|
||||||
<Grid gutter={0}>
|
<Grid gutter={0}>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }}>
|
||||||
@@ -74,13 +78,29 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
<Badge color="blue" variant="light" mb="md">
|
<Badge color="blue" variant="light" mb="md">
|
||||||
{featured.kategoriBerita?.name || kategori}
|
{featured.kategoriBerita?.name || kategori}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Title order={2} mb="md">{featured.judul}</Title>
|
<Title order={3} mb="md">
|
||||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsi }} />
|
{featured.judul}
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
mb="md"
|
||||||
|
style={{ lineHeight: 1.6 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: featured.deskripsi }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Group justify="apart" mt="auto">
|
<Group justify="apart" mt="auto">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCalendar size={18} />
|
<IconCalendar size={18} />
|
||||||
<Text size="sm">
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
c="dimmed"
|
||||||
|
lh={1.5}
|
||||||
|
style={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: '1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@@ -91,7 +111,9 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
rightSection={<IconArrowRight size={16} />}
|
rightSection={<IconArrowRight size={16} />}
|
||||||
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Baca Selengkapnya
|
Baca Selengkapnya
|
||||||
</Button>
|
</Button>
|
||||||
@@ -105,19 +127,29 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
|
|
||||||
{/* === Daftar Berita === */}
|
{/* === Daftar Berita === */}
|
||||||
<Box mt={50}>
|
<Box mt={50}>
|
||||||
<Title order={2} mb="md">Daftar Berita</Title>
|
<Title order={2} mb="md">
|
||||||
|
Daftar Berita
|
||||||
|
</Title>
|
||||||
<Divider mb="xl" />
|
<Divider mb="xl" />
|
||||||
|
|
||||||
{state.findMany.loading ? (
|
{state.findMany.loading ? (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||||
{Array(3).fill(0).map((_, i) => (
|
{Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
<Skeleton key={i} h={300} radius="md" />
|
<Skeleton key={i} h={300} radius="md" />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : paginatedNews.length === 0 ? (
|
) : paginatedNews.length === 0 ? (
|
||||||
<Text c="dimmed" ta="center">Belum ada berita di kategori "{kategori}".</Text>
|
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
Belum ada berita di kategori "{kategori}".
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
<SimpleGrid
|
||||||
|
cols={{ base: 1, sm: 2, lg: 3 }}
|
||||||
|
spacing="xl"
|
||||||
|
verticalSpacing="xl"
|
||||||
|
>
|
||||||
{paginatedNews.map((item) => (
|
{paginatedNews.map((item) => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -125,19 +157,51 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
withBorder
|
withBorder
|
||||||
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)
|
||||||
|
}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
|
<Image
|
||||||
|
src={item.image?.link}
|
||||||
|
height={200}
|
||||||
|
alt={item.judul}
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Badge color="blue" variant="light" mt="md">
|
<Badge color="blue" variant="light" mt="md">
|
||||||
{item.kategoriBerita?.name || kategori}
|
{item.kategoriBerita?.name || kategori}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
<Title
|
||||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
order={4}
|
||||||
|
mt="sm"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
style={{ lineHeight: 1.4 }}
|
||||||
|
lineClamp={2}
|
||||||
|
>
|
||||||
|
{item.judul}
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
mt="xs"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
<Group justify="apart" mt="md" gap="xs">
|
<Group justify="apart" mt="md" gap="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'xs' }}
|
||||||
|
c="dimmed"
|
||||||
|
lh={1.4}
|
||||||
|
style={{ fontSize: '0.75rem', lineHeight: '1.125rem' }}
|
||||||
|
>
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
|||||||
@@ -3,18 +3,16 @@
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||||
const state = useProxy(stateDashboardBerita.berita)
|
const state = useProxy(stateDashboardBerita.berita);
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -27,9 +25,9 @@ function Page() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
loadData()
|
loadData();
|
||||||
}, [id])
|
}, [id]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -47,41 +45,49 @@ function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
<Stack pos="relative" bg={colors.Bg} pb="xl" gap="xs" px={{ base: 'md', md: 0 }}>
|
||||||
<Group px={{ base: "md", md: 100 }}>
|
<Group px={{ base: 'md', md: 100 }}>
|
||||||
<NewsReader />
|
<NewsReader />
|
||||||
</Group>
|
</Group>
|
||||||
<Container w={{ base: "100%", md: "50%" }} >
|
<Container w={{ base: '100%', md: '50%' }}>
|
||||||
<Box pb={20}>
|
<Box pb={20}>
|
||||||
<Text id='news-title' ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
|
<Title
|
||||||
{state.findUnique.data?.judul}
|
id="news-title"
|
||||||
</Text>
|
order={1}
|
||||||
<Text
|
ta="center"
|
||||||
ta={"center"}
|
c={colors['blue-button']}
|
||||||
fw={"bold"}
|
fw="bold"
|
||||||
fz={"1.5rem"}
|
lh={{ base: 1.2, md: 1.25 }}
|
||||||
|
>
|
||||||
|
{state.findUnique.data.judul}
|
||||||
|
</Title>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
ta="center"
|
||||||
|
fw="bold"
|
||||||
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
|
lh={{ base: 1.3, md: 1.35 }}
|
||||||
>
|
>
|
||||||
Informasi dan Pelayanan Administrasi Digital
|
Informasi dan Pelayanan Administrasi Digital
|
||||||
</Text>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
|
<Image src={state.findUnique.data.image?.link || ''} alt="" w="100%" loading="lazy" />
|
||||||
</Container>
|
</Container>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap="xs">
|
||||||
<Text
|
<Text
|
||||||
id='news-content'
|
id="news-content"
|
||||||
py={20}
|
py={20}
|
||||||
fz={{ base: "sm", md: "lg" }}
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
lh={{ base: 1.6, md: 1.8 }}
|
||||||
ta="justify"
|
ta="justify"
|
||||||
style={{
|
style={{
|
||||||
wordBreak: "break-word",
|
wordBreak: 'break-word',
|
||||||
whiteSpace: "normal",
|
whiteSpace: 'normal',
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: state.findUnique.data?.content || "",
|
__html: state.findUnique.data.content || '',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,12 +1,44 @@
|
|||||||
// app/desa/berita/BeritaLayoutClient.tsx
|
// app/darmasaba/(pages)/desa/berita/layout.tsx
|
||||||
'use client'
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Box } from '@mantine/core';
|
||||||
|
import BackButton from '../layanan/_com/BackButto';
|
||||||
|
import colors from '@/con/colors';
|
||||||
const LayoutTabsBerita = dynamic(
|
const LayoutTabsBerita = dynamic(
|
||||||
() => import('./_lib/layoutTabs'),
|
() => import('./_lib/layoutTabs'),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function BeritaLayoutClient({ children }: { children: React.ReactNode }) {
|
export default function BeritaLayoutClient({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Contoh path:
|
||||||
|
// - /darmasaba/desa/berita/semua → panjang 5 → list
|
||||||
|
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
|
||||||
|
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
|
||||||
|
|
||||||
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
|
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||||
|
|
||||||
|
if (isDetailPage) {
|
||||||
|
// Tampilkan tanpa tab menu
|
||||||
|
return (
|
||||||
|
<Box bg={colors.Bg}>
|
||||||
|
<Box pt={33} px={{ base: 'md', md: 100 }}>
|
||||||
|
<BackButton />
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
|
||||||
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
|
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
|
||||||
}
|
}
|
||||||
@@ -16,35 +16,30 @@ function Semua() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
|
||||||
// Ambil parameter langsung dari URL
|
|
||||||
const search = searchParams.get('search') || '';
|
const search = searchParams.get('search') || '';
|
||||||
const page = parseInt(searchParams.get('page') || '1');
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
|
||||||
// Gunakan proxy untuk state global
|
|
||||||
const state = useProxy(stateDashboardBerita.berita);
|
const state = useProxy(stateDashboardBerita.berita);
|
||||||
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
||||||
const loadingGrid = state.findMany.loading;
|
const loadingGrid = state.findMany.loading;
|
||||||
const loadingFeatured = featured.loading;
|
const loadingFeatured = featured.loading;
|
||||||
|
|
||||||
// Load berita utama sekali saja
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!featured.data && !loadingFeatured) {
|
if (!featured.data && !loadingFeatured) {
|
||||||
stateDashboardBerita.berita.findFirst.load();
|
stateDashboardBerita.berita.findFirst.load();
|
||||||
}
|
}
|
||||||
}, [featured.data, loadingFeatured]);
|
}, [featured.data, loadingFeatured]);
|
||||||
|
|
||||||
// Load berita terbaru tiap page / search berubah
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const limit = 3;
|
const limit = 3;
|
||||||
state.findMany.load(page, limit, search);
|
state.findMany.load(page, limit, search);
|
||||||
}, [page, search]);
|
}, [page, search]);
|
||||||
|
|
||||||
// Handler pagination → langsung update URL
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
const url = new URLSearchParams(searchParams.toString());
|
const url = new URLSearchParams(searchParams.toString());
|
||||||
if (search) url.set('search', search);
|
if (search) url.set('search', search);
|
||||||
if (newPage > 1) url.set('page', newPage.toString());
|
if (newPage > 1) url.set('page', newPage.toString());
|
||||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
else url.delete('page');
|
||||||
|
|
||||||
router.replace(`?${url.toString()}`);
|
router.replace(`?${url.toString()}`);
|
||||||
};
|
};
|
||||||
@@ -61,7 +56,7 @@ function Semua() {
|
|||||||
<Center><Skeleton h={400} /></Center>
|
<Center><Skeleton h={400} /></Center>
|
||||||
) : featuredData ? (
|
) : featuredData ? (
|
||||||
<Box mb={50}>
|
<Box mb={50}>
|
||||||
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
|
<Title order={2} mb="md">Berita Utama</Title>
|
||||||
<Paper shadow="md" radius="md" withBorder>
|
<Paper shadow="md" radius="md" withBorder>
|
||||||
<Grid gutter={0}>
|
<Grid gutter={0}>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }}>
|
||||||
@@ -81,13 +76,24 @@ function Semua() {
|
|||||||
<Badge color="blue" variant="light" mb="md">
|
<Badge color="blue" variant="light" mb="md">
|
||||||
{featuredData.kategoriBerita?.name || 'Berita'}
|
{featuredData.kategoriBerita?.name || 'Berita'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
<Title order={3} mb="md">{featuredData.judul}</Title>
|
||||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }} />
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
mb="md"
|
||||||
|
dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Group justify="apart" mt="auto">
|
<Group justify="apart" mt="auto">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCalendar size={18} />
|
<IconCalendar size={18} />
|
||||||
<Text size="sm">
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
lh={{ base: 1.4, md: 1.5 }}
|
||||||
|
>
|
||||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@@ -124,7 +130,9 @@ function Semua() {
|
|||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : paginatedNews.length === 0 ? (
|
) : paginatedNews.length === 0 ? (
|
||||||
<Text c="dimmed" ta="center">Tidak ada berita ditemukan.</Text>
|
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={{ base: 1.5, md: 1.6 }}>
|
||||||
|
Tidak ada berita ditemukan.
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||||
{paginatedNews.map((item) => (
|
{paginatedNews.map((item) => (
|
||||||
@@ -143,11 +151,24 @@ function Semua() {
|
|||||||
{item.kategoriBerita?.name || 'Berita'}
|
{item.kategoriBerita?.name || 'Berita'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
<Title order={4} mt="sm" lineClamp={2}>
|
||||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
{item.judul}
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
mt="xs"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
fz={{ base: 'xs', md: 'xs' }}
|
||||||
|
lh={{ base: 1.4, md: 1.4 }}
|
||||||
|
>
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Center, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
|
|
||||||
interface FileItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
realName: string;
|
|
||||||
createdAt: string | Date;
|
|
||||||
category: string;
|
|
||||||
path: string;
|
|
||||||
mimeType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FotoContent() {
|
|
||||||
const [files, setFiles] = useState<FileItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
|
||||||
const limit = 9; // ✅ ambil 12 data per page
|
|
||||||
|
|
||||||
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const query: Record<string, string> = {
|
|
||||||
category: 'image',
|
|
||||||
page: pageNum.toString(),
|
|
||||||
limit: limit.toString(),
|
|
||||||
};
|
|
||||||
if (searchTerm) query.search = searchTerm;
|
|
||||||
|
|
||||||
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
|
|
||||||
|
|
||||||
if (response.status === 200 && response.data) {
|
|
||||||
setFiles(response.data.data || []);
|
|
||||||
setTotalPages(response.data.meta?.totalPages || 1);
|
|
||||||
} else {
|
|
||||||
setFiles([]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Load error:', err);
|
|
||||||
setFiles([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ✅ Initial load + update when URL/search changes
|
|
||||||
useEffect(() => {
|
|
||||||
const handleRouteChange = () => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const urlSearch = urlParams.get('search') || '';
|
|
||||||
const urlPage = parseInt(urlParams.get('page') || '1');
|
|
||||||
setSearch(urlSearch);
|
|
||||||
setPage(urlPage);
|
|
||||||
loadData(urlPage, urlSearch);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearchUpdate = (e: Event) => {
|
|
||||||
const { search } = (e as CustomEvent).detail;
|
|
||||||
setSearch(search);
|
|
||||||
setPage(1);
|
|
||||||
loadData(1, search);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRouteChange();
|
|
||||||
window.addEventListener('popstate', handleRouteChange);
|
|
||||||
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('popstate', handleRouteChange);
|
|
||||||
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
|
||||||
};
|
|
||||||
}, [loadData]);
|
|
||||||
|
|
||||||
// ✅ Update when page/search changes
|
|
||||||
useEffect(() => {
|
|
||||||
loadData(page, search);
|
|
||||||
}, [page, search, loadData]);
|
|
||||||
|
|
||||||
const updateURL = (newSearch: string, newPage: number) => {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
if (newSearch) url.searchParams.set('search', newSearch);
|
|
||||||
else url.searchParams.delete('search');
|
|
||||||
if (newPage > 1) url.searchParams.set('page', newPage.toString());
|
|
||||||
else url.searchParams.delete('page');
|
|
||||||
window.history.pushState({}, '', url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
setPage(newPage);
|
|
||||||
updateURL(search, newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading && files.length === 0) {
|
|
||||||
return <Center>Memuat data...</Center>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
return <Center>Tidak ada foto ditemukan</Center>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
|
||||||
{files.map((file) => (
|
|
||||||
<Paper
|
|
||||||
key={file.id}
|
|
||||||
mb={50}
|
|
||||||
p="md"
|
|
||||||
radius={26}
|
|
||||||
bg={colors['white-trans-1']}
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
>
|
|
||||||
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
|
|
||||||
<Image
|
|
||||||
src={file.link}
|
|
||||||
alt={file.realName || file.name}
|
|
||||||
height={250}
|
|
||||||
width="100%"
|
|
||||||
style={{ objectFit: 'cover', height: '100%', width: '100%' }}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Stack gap="sm" py={10}>
|
|
||||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
|
||||||
{file.realName || file.name}
|
|
||||||
</Text>
|
|
||||||
<Text fz="sm" c="dimmed">
|
|
||||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
<Center mt="xl">
|
|
||||||
<Pagination total={totalPages} value={page} onChange={handlePageChange} />
|
|
||||||
</Center>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,173 @@
|
|||||||
'use client'
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import { Suspense } from 'react';
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Grid,
|
||||||
|
Image,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconPhoto } from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
// ✅ Load komponen tanpa SSR
|
// Komponen kartu foto
|
||||||
const FotoContent = dynamic(
|
function FotoCard({ item }: { item: any }) {
|
||||||
() => import('./Content'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div>Memuat konten...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function PageContent() {
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Memuat...</div>}>
|
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
|
||||||
<FotoContent />
|
<Paper
|
||||||
</Suspense>
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
p={0}
|
||||||
|
style={{ transition: 'transform 0.2s' }}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||||
|
|
||||||
|
>
|
||||||
|
{item.imageGalleryFoto?.link ? (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
style={{
|
||||||
|
paddingBottom: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: '4px 4px 0 0',
|
||||||
|
backgroundColor: '#f9f9f9',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
radius="lg"
|
||||||
|
src={item.imageGalleryFoto.link}
|
||||||
|
alt={item.name || 'Foto Galeri'}
|
||||||
|
p={10}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
objectPosition: 'center',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Center h={180} bg="gray.1">
|
||||||
|
<IconPhoto size={40} color="gray" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack p="md" gap={4}>
|
||||||
|
<Text fw={600} lineClamp={1} fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
|
||||||
|
{item.name || 'Tanpa Judul'}
|
||||||
|
</Text>
|
||||||
|
{item.deskripsi && (
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={2}
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
lh={{ base: '1.4', md: '1.5' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
fz={{ base: 11, md: 'xs' }}
|
||||||
|
c="dimmed"
|
||||||
|
lh={{ base: '1.3', md: '1.4' }}
|
||||||
|
>
|
||||||
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid.Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
// Komponen utama
|
||||||
return <PageContent />;
|
export default function GaleriFotoUser() {
|
||||||
|
const [search] = useState('');
|
||||||
|
return (
|
||||||
|
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Title order={2} c={colors['blue-button']} mb="lg" ta="center">
|
||||||
|
Galeri Foto Desa Darmasaba
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* Daftar Foto */}
|
||||||
|
<FotoList search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FotoList({ search }: { search: string }) {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = FotoState.findMany;
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
load(page, 3, search);
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Grid mt="md">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Grid.Col key={i} span={{ base: 12, xs: 6, md: 4 }}>
|
||||||
|
<Skeleton height={280} radius="md" />
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center py="xl">
|
||||||
|
<Stack align="center" c="dimmed">
|
||||||
|
<IconPhoto size={48} />
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
|
||||||
|
Tidak ada foto ditemukan
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack mt="md" gap="xl">
|
||||||
|
<Grid>
|
||||||
|
{data.map((item) => (
|
||||||
|
<FotoCard key={item.id} item={item} />
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 3, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { ReactNode } from "react";
|
||||||
import LayoutTabsGalery from "./_lib/layoutTabs";
|
import LayoutTabsGalery from "./_lib/layoutTabs";
|
||||||
|
|
||||||
export default function LayoutGalery({ children }: { children: React.ReactNode }) {
|
// export default function LayoutGalery({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
// return (
|
||||||
<LayoutTabsGalery>
|
// <LayoutTabsGalery>
|
||||||
{children}
|
// {children}
|
||||||
</LayoutTabsGalery>
|
// </LayoutTabsGalery>
|
||||||
)
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default function BeritaLayoutClient({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Contoh path:
|
||||||
|
// - /darmasaba/desa/berita/semua → panjang 5 → list
|
||||||
|
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
|
||||||
|
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
|
||||||
|
|
||||||
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
|
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||||
|
|
||||||
|
if (isDetailPage) {
|
||||||
|
// Tampilkan tanpa tab menu
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
|
||||||
|
return <LayoutTabsGalery>{children}</LayoutTabsGalery>;
|
||||||
}
|
}
|
||||||
@@ -4,29 +4,29 @@ import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Center,
|
Center,
|
||||||
|
Group,
|
||||||
Pagination,
|
Pagination,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Spoiler,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useTransitionRouter } from 'next-view-transitions';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
export default function VideoContent() {
|
export default function VideoContent() {
|
||||||
// ✅ expanded state per index
|
|
||||||
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
|
|
||||||
const videoState = useSnapshot(stateGallery.video);
|
const videoState = useSnapshot(stateGallery.video);
|
||||||
|
const router = useTransitionRouter();
|
||||||
const { data, page, totalPages, loading } = videoState.findMany;
|
const { data, page, totalPages, loading } = videoState.findMany;
|
||||||
|
|
||||||
// Handle search and pagination changes
|
|
||||||
const loadData = useCallback((pageNum: number, searchTerm: string) => {
|
const loadData = useCallback((pageNum: number, searchTerm: string) => {
|
||||||
stateGallery.video.findMany.load(pageNum, 10, searchTerm.trim());
|
stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initial load and URL change handler
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -56,19 +56,14 @@ export default function VideoContent() {
|
|||||||
loadData(newPage, search);
|
loadData(newPage, search);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleExpanded = (index: number, value: boolean) => {
|
|
||||||
setExpandedMap((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[index]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataVideo = data || [];
|
const dataVideo = data || [];
|
||||||
|
|
||||||
if (loading && !data) {
|
if (loading && !data) {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Text>Memuat Video...</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" ta="center">
|
||||||
|
Memuat Video...
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,9 +78,8 @@ export default function VideoContent() {
|
|||||||
p="md"
|
p="md"
|
||||||
radius={26}
|
radius={26}
|
||||||
bg={colors['white-trans-1']}
|
bg={colors['white-trans-1']}
|
||||||
w={{ base: '100%', md: '100%' }}
|
w="100%"
|
||||||
>
|
>
|
||||||
<Box>
|
|
||||||
<Center>
|
<Center>
|
||||||
<Box
|
<Box
|
||||||
component="iframe"
|
component="iframe"
|
||||||
@@ -97,46 +91,58 @@ export default function VideoContent() {
|
|||||||
style={{ borderRadius: 8 }}
|
style={{ borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Stack gap="sm" py={10}>
|
<Stack gap="sm" py={10}>
|
||||||
<Text fz="sm" c="dimmed">
|
{/* Tanggal: Caption */}
|
||||||
|
<Text
|
||||||
|
fz={{ base: 12, md: 14 }}
|
||||||
|
c="dimmed"
|
||||||
|
ta="left"
|
||||||
|
>
|
||||||
{new Date(v.createdAt).toLocaleDateString('id-ID', {
|
{new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fw="bold" fz="sm" lineClamp={1}>
|
|
||||||
{v.name}
|
{/* Judul Video: Subsection (H3) */}
|
||||||
</Text>
|
<Title
|
||||||
<Spoiler
|
order={3}
|
||||||
showLabel={
|
c="dark"
|
||||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
ta="left"
|
||||||
Show more
|
lh={1.3}
|
||||||
</Text>
|
style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
||||||
}
|
|
||||||
hideLabel={
|
|
||||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
|
||||||
Hide details
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
expanded={expandedMap[k] || false}
|
|
||||||
onExpandedChange={(val) => toggleExpanded(k, val)}
|
|
||||||
>
|
>
|
||||||
|
{v.name}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* Deskripsi: Body kecil */}
|
||||||
<Text
|
<Text
|
||||||
ta="justify"
|
ta="justify"
|
||||||
fz="sm"
|
fz={{ base: 13, md: 14 }}
|
||||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
c="dimmed"
|
||||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
style={{ wordBreak: 'break-word' }}
|
||||||
/>
|
lineClamp={3}
|
||||||
</Spoiler>
|
>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -150,7 +156,6 @@ export default function VideoContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fix: convert YouTube URL ke embed
|
|
||||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(youtubeUrl);
|
const url = new URL(youtubeUrl);
|
||||||
|
|||||||
209
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
209
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
|
import BackButton from '../../../layanan/_com/BackButto';
|
||||||
|
|
||||||
|
|
||||||
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
|
try {
|
||||||
|
const url = new URL(youtubeUrl);
|
||||||
|
let videoId = '';
|
||||||
|
|
||||||
|
if (url.hostname === 'youtu.be') {
|
||||||
|
videoId = url.pathname.slice(1);
|
||||||
|
} else if (url.hostname.includes('youtube.com')) {
|
||||||
|
videoId = url.searchParams.get('v') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoId ? `https://www.youtube.com/embed/${videoId}` : youtubeUrl;
|
||||||
|
} catch {
|
||||||
|
return youtubeUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DetailVideoUser() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const router = useRouter();
|
||||||
|
const videoState = useProxy(stateGallery.video);
|
||||||
|
const [videoError, setVideoError] = useState(false);
|
||||||
|
|
||||||
|
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
videoState.findUnique.load(id);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const data = videoState.findUnique.data;
|
||||||
|
|
||||||
|
if (!videoState.findUnique && !id) {
|
||||||
|
return (
|
||||||
|
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||||
|
<Skeleton height={400} radius="md" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||||
|
<Alert
|
||||||
|
icon={<IconInfoCircle size={20} />}
|
||||||
|
title="Video tidak ditemukan"
|
||||||
|
color="red"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} c="red.9">
|
||||||
|
Video yang Anda cari tidak tersedia.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconArrowBack size={16} />}
|
||||||
|
mt="md"
|
||||||
|
onClick={() => router.push('/darmasaba/galeri/video')}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Kembali ke Galeri
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrl = data.linkVideo ? convertToEmbedUrl(data.linkVideo) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py="xl" px={{ base: 'md', md: 100 }}>
|
||||||
|
{/* Tombol Kembali */}
|
||||||
|
<Box>
|
||||||
|
<BackButton />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Header - Dijadikan Title */}
|
||||||
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta="center"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
mb="lg"
|
||||||
|
lh={{ base: 1.2, md: 1.25 }}
|
||||||
|
>
|
||||||
|
{data.name || 'Video Galeri Desa'}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* Konten Utama */}
|
||||||
|
<Card
|
||||||
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
p={{ base: 'md', md: 'xl' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Video */}
|
||||||
|
{embedUrl ? (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
src={embedUrl}
|
||||||
|
title={data.name}
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
onError={() => setVideoError(true)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : videoError ? (
|
||||||
|
<Alert
|
||||||
|
color="orange"
|
||||||
|
icon={<IconVideo size={20} />}
|
||||||
|
title="Gagal memuat video"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Text fz={{ base: 'xs', md: 'sm' }} c="orange.9">
|
||||||
|
Mohon maaf, video tidak dapat diputar.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
color="gray"
|
||||||
|
icon={<IconInfoCircle size={20} />}
|
||||||
|
title="Tidak ada video"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed">
|
||||||
|
Konten video belum tersedia.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Informasi Tambahan */}
|
||||||
|
{data.createdAt && (
|
||||||
|
<Group gap="xs" justify="center" wrap="nowrap">
|
||||||
|
<ThemeIcon variant="light" size="sm" radius="xl">
|
||||||
|
<IconInfoCircle size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
c="dimmed"
|
||||||
|
lh={{ base: 1.4, md: 1.5 }}
|
||||||
|
>
|
||||||
|
Diunggah pada{' '}
|
||||||
|
{new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
{data.deskripsi && (
|
||||||
|
<Paper p="md" bg="gray.0" radius="md">
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
c="dark"
|
||||||
|
ta={"justify"}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -100,7 +100,7 @@ function Page() {
|
|||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Container>
|
</Container>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "35", md: 100 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text
|
<Text
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
|
import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -39,30 +39,38 @@ function PelayananPendudukNonPermanent() {
|
|||||||
) : (
|
) : (
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={{ base: "xl", md: "2xl" }} fw={700} lh={1.3} c="dark">
|
<Title
|
||||||
|
order={1}
|
||||||
|
fz={{ base: 'lg', md: 'xl' }}
|
||||||
|
fw={700}
|
||||||
|
lh={{ base: 1.3, md: 1.3 }}
|
||||||
|
c="dark"
|
||||||
|
>
|
||||||
{data?.name || "Judul belum tersedia"}
|
{data?.name || "Judul belum tersedia"}
|
||||||
</Text>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{data?.deskripsi ? (
|
{data?.deskripsi ? (
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: "sm", md: "md" }}
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
lh={1.7}
|
lh={{ base: 1.6, md: 1.7 }}
|
||||||
ta="justify"
|
ta="justify"
|
||||||
c="dimmed"
|
c="black"
|
||||||
dangerouslySetInnerHTML={{ __html: data?.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data?.deskripsi }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text fz="sm" c="gray">Deskripsi belum tersedia.</Text>
|
<Text fz="xs" c="gray">
|
||||||
|
Deskripsi belum tersedia.
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider color={colors["blue-button"]} size="sm" />
|
<Divider color={colors["blue-button"]} size="sm" />
|
||||||
|
|
||||||
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
<Text fz={{ base: "xs", md: "sm" }} c="dimmed">
|
<Text fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.5 }} c="black">
|
||||||
25 Mei 2021 • Darmasaba
|
25 Mei 2021 • Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function PelayananPerizinanBerusaha() {
|
|||||||
return (
|
return (
|
||||||
<Center mih={300}>
|
<Center mih={300}>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Text fz="lg" fw={500} c="dimmed">
|
<Text fz={{ base: 'md', md: 'lg' }} fw={500} c="dimmed" lh="sm">
|
||||||
Belum ada informasi layanan yang tersedia
|
Belum ada informasi layanan yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
|
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
|
||||||
@@ -67,10 +67,10 @@ function PelayananPerizinanBerusaha() {
|
|||||||
) : (
|
) : (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
|
<Title order={2} fw={700} mb="sm">
|
||||||
Perizinan Berusaha Berbasis Risiko melalui OSS
|
Perizinan Berusaha Berbasis Risiko melalui OSS
|
||||||
</Title>
|
</Title>
|
||||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
<Text fz={{ base: 'sm', md: 'md' }} c="black" lh="sm">
|
||||||
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
|
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -83,13 +83,13 @@ function PelayananPerizinanBerusaha() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
|
<Title order={3} fw={600} mb="sm">
|
||||||
Alur pendaftaran NIB:
|
Alur pendaftaran NIB:
|
||||||
</Text>
|
</Title>
|
||||||
<Stepper
|
<Stepper
|
||||||
active={active}
|
active={active}
|
||||||
onStepClick={(step) => {
|
onStepClick={(step) => {
|
||||||
if (step <= active) { // Only allow clicking on previous or current steps
|
if (step <= active) {
|
||||||
setActive(step);
|
setActive(step);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -102,28 +102,42 @@ function PelayananPerizinanBerusaha() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StepperStep label="Langkah 1" description="Daftar Akun">
|
<StepperStep label="Langkah 1" description="Daftar Akun">
|
||||||
<Text fz="sm">Membuat akun di portal OSS</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Membuat akun di portal OSS
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
|
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
|
||||||
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Lengkapi informasi perusahaan, data pemegang saham, dan alamat
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperStep label="Langkah 3" description="Pilih KBLI">
|
<StepperStep label="Langkah 3" description="Pilih KBLI">
|
||||||
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Menentukan kode KBLI sesuai jenis usaha
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperStep label="Langkah 4" description="Unggah Dokumen">
|
<StepperStep label="Langkah 4" description="Unggah Dokumen">
|
||||||
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Unggah akta pendirian, surat izin, dan dokumen wajib lainnya
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
|
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
|
||||||
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Menunggu verifikasi dan persetujuan dari pihak berwenang
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperStep label="Langkah 6" description="Terbit NIB">
|
<StepperStep label="Langkah 6" description="Terbit NIB">
|
||||||
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||||
|
Menerima NIB sebagai identitas resmi usaha
|
||||||
|
</Text>
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
<StepperCompleted>
|
<StepperCompleted>
|
||||||
<Center>
|
<Center>
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<IconCheck size={40} color="green" />
|
<IconCheck size={40} color="green" />
|
||||||
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh="sm">
|
||||||
|
Proses pendaftaran selesai
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
</StepperCompleted>
|
</StepperCompleted>
|
||||||
@@ -159,7 +173,7 @@ function PelayananPerizinanBerusaha() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
<Text fz={{ base: 'xs', md: 'sm' }} ta="justify" c="black" lh="sm" mt="md">
|
||||||
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
|
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
|
||||||
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
|
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
|
||||||
oss.go.id
|
oss.go.id
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react';
|
import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -35,7 +35,7 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
|||||||
<Center py="xl">
|
<Center py="xl">
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} />
|
<IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} />
|
||||||
<Text c="dimmed" ta="center">
|
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh="sm">
|
||||||
Tidak ada layanan surat keterangan yang ditemukan
|
Tidak ada layanan surat keterangan yang ditemukan
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -48,9 +48,9 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
|||||||
<Group justify="space-between" align="center" mb="md">
|
<Group justify="space-between" align="center" mb="md">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconFileDescription size={28} stroke={1.8} />
|
<IconFileDescription size={28} stroke={1.8} />
|
||||||
<Text fz={{ base: "h4", md: "h2" }} fw={700}>
|
<Title order={2} c="black">
|
||||||
Layanan Surat Keterangan
|
Layanan Surat Keterangan
|
||||||
</Text>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
|
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
|
||||||
<IconInfoCircle size={22} stroke={1.8} />
|
<IconInfoCircle size={22} stroke={1.8} />
|
||||||
@@ -82,15 +82,15 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
|||||||
style={{ borderRadius: 16 }}
|
style={{ borderRadius: 16 }}
|
||||||
/>
|
/>
|
||||||
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
|
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
|
||||||
<Text
|
<Title
|
||||||
|
order={3}
|
||||||
c="white"
|
c="white"
|
||||||
fw={600}
|
|
||||||
fz="lg"
|
|
||||||
ta="center"
|
ta="center"
|
||||||
lineClamp={2}
|
lineClamp={2}
|
||||||
|
lh="sm"
|
||||||
>
|
>
|
||||||
{v.name}
|
{v.name}
|
||||||
</Text>
|
</Title>
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -42,9 +42,10 @@ function PelayananTelunjukSaktiDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={2} mb="lg" fz={{ base: 22, md: 28 }} fw={700} style={{ lineHeight: 1.4 }}>
|
<Title order={2} mb="lg" fw={700} style={{ lineHeight: 1.3 }} ta="left">
|
||||||
Layanan Telunjuk Sakti Desa <br />
|
Layanan Telunjuk Sakti Desa
|
||||||
<Text span c="dimmed" fz="lg" fw={400}>
|
<Text span c="black" fz={{ base: 'sm', md: 'md' }} fw={400} style={{ lineHeight: 1.5 }}>
|
||||||
|
{' '}
|
||||||
Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman
|
Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman
|
||||||
</Text>
|
</Text>
|
||||||
</Title>
|
</Title>
|
||||||
@@ -53,7 +54,7 @@ function PelayananTelunjukSaktiDesa() {
|
|||||||
<Skeleton h={400} radius="lg" />
|
<Skeleton h={400} radius="lg" />
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<Card shadow="sm" radius="lg" withBorder>
|
<Card shadow="sm" radius="lg" withBorder>
|
||||||
<Text c="dimmed" ta="center" py="xl">
|
<Text c="black" ta="center" py="xl" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
Belum ada layanan tersedia untuk saat ini
|
Belum ada layanan tersedia untuk saat ini
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -72,9 +73,9 @@ function PelayananTelunjukSaktiDesa() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Text fw={700} fz="lg" lh={1.4}>
|
<Title order={3} fw={700} lh={1.3}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</Text>
|
</Title>
|
||||||
<Flex gap="xs" align="center">
|
<Flex gap="xs" align="center">
|
||||||
<IconExternalLink size={18} stroke={1.5} />
|
<IconExternalLink size={18} stroke={1.5} />
|
||||||
<Text
|
<Text
|
||||||
@@ -82,7 +83,7 @@ function PelayananTelunjukSaktiDesa() {
|
|||||||
href={v.link}
|
href={v.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
fz="sm"
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
c="blue"
|
c="blue"
|
||||||
td="underline"
|
td="underline"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
|
|
||||||
tanggal: 'Jumat, 26 April 2025',
|
|
||||||
jam: '16:00 WITA',
|
|
||||||
lokasi: 'Wantilan Adat Desa',
|
|
||||||
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
|
|
||||||
tanggal: 'Jumat, 26 April 2025',
|
|
||||||
jam: '16:00 WITA',
|
|
||||||
lokasi: 'Wantilan Adat Desa',
|
|
||||||
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
|
|
||||||
tanggal: 'Jumat, 26 April 2025',
|
|
||||||
jam: '16:00 WITA',
|
|
||||||
lokasi: 'Wantilan Adat Desa',
|
|
||||||
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
|
|
||||||
tanggal: 'Jumat, 26 April 2025',
|
|
||||||
jam: '16:00 WITA',
|
|
||||||
lokasi: 'Wantilan Adat Desa',
|
|
||||||
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
|
|
||||||
tanggal: 'Jumat, 26 April 2025',
|
|
||||||
jam: '16:00 WITA',
|
|
||||||
lokasi: 'Wantilan Adat Desa',
|
|
||||||
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Adat & Budaya
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman adat & budaya di Desa Darmasaba.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
|
|
||||||
tanggal: 'Selasa, 30 April 2025',
|
|
||||||
jam: '09:00 WITA',
|
|
||||||
lokasi: 'Perpustakaan Desa',
|
|
||||||
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
|
|
||||||
tanggal: 'Selasa, 30 April 2025',
|
|
||||||
jam: '09:00 WITA',
|
|
||||||
lokasi: 'Perpustakaan Desa',
|
|
||||||
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
|
|
||||||
tanggal: 'Selasa, 30 April 2025',
|
|
||||||
jam: '09:00 WITA',
|
|
||||||
lokasi: 'Perpustakaan Desa',
|
|
||||||
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
|
|
||||||
tanggal: 'Selasa, 30 April 2025',
|
|
||||||
jam: '09:00 WITA',
|
|
||||||
lokasi: 'Perpustakaan Desa',
|
|
||||||
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
|
|
||||||
tanggal: 'Selasa, 30 April 2025',
|
|
||||||
jam: '09:00 WITA',
|
|
||||||
lokasi: 'Perpustakaan Desa',
|
|
||||||
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Digitalisasi Desa
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman digitalisasi desa
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
|
|
||||||
tanggal: 'Rabu, 23 April 2025',
|
|
||||||
jam: '13:00 WITA',
|
|
||||||
lokasi: 'Aula Kantor Desa',
|
|
||||||
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
|
|
||||||
tanggal: 'Rabu, 23 April 2025',
|
|
||||||
jam: '13:00 WITA',
|
|
||||||
lokasi: 'Aula Kantor Desa',
|
|
||||||
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
|
|
||||||
tanggal: 'Rabu, 23 April 2025',
|
|
||||||
jam: '13:00 WITA',
|
|
||||||
lokasi: 'Aula Kantor Desa',
|
|
||||||
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
|
|
||||||
tanggal: 'Rabu, 23 April 2025',
|
|
||||||
jam: '13:00 WITA',
|
|
||||||
lokasi: 'Aula Kantor Desa',
|
|
||||||
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
|
|
||||||
tanggal: 'Rabu, 23 April 2025',
|
|
||||||
jam: '13:00 WITA',
|
|
||||||
lokasi: 'Aula Kantor Desa',
|
|
||||||
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Ekonomi & UMKM
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman ekonomi & umkm
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Gotong Royong Bersih Sungai dan Drainase',
|
|
||||||
tanggal: 'Minggu, 21 April 2025',
|
|
||||||
jam: '06:30 WITA',
|
|
||||||
lokasi: 'Titik Kumpul: Poskamling RW 02',
|
|
||||||
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Gotong Royong Bersih Sungai dan Drainase',
|
|
||||||
tanggal: 'Minggu, 21 April 2025',
|
|
||||||
jam: '06:30 WITA',
|
|
||||||
lokasi: 'Titik Kumpul: Poskamling RW 02',
|
|
||||||
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Gotong Royong Bersih Sungai dan Drainase',
|
|
||||||
tanggal: 'Minggu, 21 April 2025',
|
|
||||||
jam: '06:30 WITA',
|
|
||||||
lokasi: 'Titik Kumpul: Poskamling RW 02',
|
|
||||||
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Gotong Royong Bersih Sungai dan Drainase',
|
|
||||||
tanggal: 'Minggu, 21 April 2025',
|
|
||||||
jam: '06:30 WITA',
|
|
||||||
lokasi: 'Titik Kumpul: Poskamling RW 02',
|
|
||||||
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Gotong Royong Bersih Sungai dan Drainase',
|
|
||||||
tanggal: 'Minggu, 21 April 2025',
|
|
||||||
jam: '06:30 WITA',
|
|
||||||
lokasi: 'Titik Kumpul: Poskamling RW 02',
|
|
||||||
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Lingkungan & Bencana
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman lingkungan & bencana
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Lomba Video Pendek Hari Lingkungan',
|
|
||||||
tanggal: 'Deadline: 28 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Online Submission',
|
|
||||||
deskripsi: 'Karang Taruna Desa mengadakan lomba video pendek bertema "Lingkunganku, Tanggung Jawabku". Pemenang akan diumumkan saat acara Hari Desa Hijau. Total hadiah Rp1.000.000.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Lomba Video Pendek Hari Lingkungan',
|
|
||||||
tanggal: 'Deadline: 28 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Online Submission',
|
|
||||||
deskripsi: 'Karang Taruna Desa mengadakan lomba video pendek bertema "Lingkunganku, Tanggung Jawabku". Pemenang akan diumumkan saat acara Hari Desa Hijau. Total hadiah Rp1.000.000.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Pendidikan & Kepemudaan
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman pendidikan & kepemudaan
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import colors from '@/con/colors';
|
|
||||||
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
const dataPengumuman = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
|
|
||||||
tanggal: 'Sabtu, 20 April 2025',
|
|
||||||
jam: '08:00 WITA',
|
|
||||||
lokasi: 'Balai Banjar Desa Darmasaba',
|
|
||||||
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
|
||||||
{/* Header */}
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<BackButton />
|
|
||||||
</Box>
|
|
||||||
<Container size="lg" px="md" >
|
|
||||||
<Stack align="center" gap="0" >
|
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
Pengumuman Sosial & Kesehatan
|
|
||||||
</Text>
|
|
||||||
<Text ta="center" px="md" pb={10}>
|
|
||||||
Informasi dan pengumuman resmi terkait pengumuman sosial & kesehatan
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
|
||||||
{dataPengumuman.map((v, k) => {
|
|
||||||
return (
|
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
<Text size="sm">{v.tanggal}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={18} />
|
|
||||||
<Text size="sm">{v.jam}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={18} />
|
|
||||||
<Text size="sm">{v.lokasi}</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Text ta={'justify'}>
|
|
||||||
{v.deskripsi}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,58 +1,94 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Container, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
|
||||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||||
|
import BackButton from '../../../layanan/_com/BackButto';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
|
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique);
|
||||||
|
const params = useParams();
|
||||||
const params = useParams()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string)
|
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!detail.data) {
|
if (!detail.data) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Skeleton h={400} />
|
<Skeleton h={400} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Container size="lg" px="md">
|
<Container size="lg" px="md">
|
||||||
<Group>
|
<Group>
|
||||||
<NewsReader />
|
<NewsReader />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group justify={"space-between"} align={"center"}>
|
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||||
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
|
<Title
|
||||||
|
order={1}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fz={{ base: 28, md: 36 }}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
{detail.data?.judul}
|
{detail.data?.judul}
|
||||||
|
</Title>
|
||||||
|
<Paper bg={colors['blue-button']} p={8} style={{ flexShrink: 0 }}>
|
||||||
|
<Text c={colors['white-1']} fz={{ base: 'xs', md: 'sm' }} lh={1.2}>
|
||||||
|
{detail.data?.CategoryPengumuman?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify='end'>
|
|
||||||
<Paper bg={colors['blue-button']} p={5}>
|
|
||||||
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
|
||||||
<Paper bg={colors["white-1"]} p="md">
|
<Paper
|
||||||
<Text id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
|
bg={colors['white-1']}
|
||||||
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
|
p="md"
|
||||||
|
w="100%"
|
||||||
|
mih={{ base: 200, md: 300 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
px="lg"
|
||||||
|
id="news-content"
|
||||||
|
fz={{ base: 14, md: 16 }}
|
||||||
|
lh={{ base: 1.6, md: 1.6 }}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: detail.data?.content }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
px="lg"
|
||||||
|
fz={{ base: 12, md: 14 }}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fw="bold"
|
||||||
|
lh={{ base: 1.4, md: 1.4 }}
|
||||||
|
mt="md"
|
||||||
|
>
|
||||||
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
|
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Container, Group, Paper, Stack, Text } from '@mantine/core';
|
import { Box, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
import { IconCalendar } from '@tabler/icons-react';
|
import { IconCalendar } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../layanan/_com/BackButto';
|
import BackButton from '../../layanan/_com/BackButto';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const unwrappedParams = useParams();
|
const unwrappedParams = useParams();
|
||||||
const kategoriState = useProxy(stateDesaPengumuman);
|
const kategoriState = useProxy(stateDesaPengumuman);
|
||||||
@@ -26,45 +25,82 @@ function Page() {
|
|||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Container size="lg" px="md">
|
<Container size="lg" px="md">
|
||||||
<Stack align="center" gap="0" >
|
<Stack align="center" gap="xs">
|
||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
<Title
|
||||||
|
order={1}
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
ta="center"
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
{categoryName.split('-').map(word =>
|
{categoryName.split('-').map(word =>
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
).join(' ')}
|
).join(' ')}
|
||||||
</Text>
|
</Title>
|
||||||
<Text ta="center" px="md" pb={10}>
|
<Text
|
||||||
|
ta="center"
|
||||||
|
px="md"
|
||||||
|
pb="sm"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
|
c="dimmed"
|
||||||
|
>
|
||||||
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
|
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
{!kategoriState.pengumuman.findMany.data?.length ? (
|
{!kategoriState.pengumuman.findMany.data?.length ? (
|
||||||
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
ta="center"
|
||||||
|
c="dimmed"
|
||||||
|
>
|
||||||
Tidak ada pengumuman yang ditemukan
|
Tidak ada pengumuman yang ditemukan
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : kategoriState.pengumuman.findMany.data?.map((v, k) => {
|
) : (
|
||||||
return (
|
kategoriState.pengumuman.findMany.data?.map((v, k) => (
|
||||||
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
<Paper
|
||||||
<Text fz={'h3'}>{v.judul}</Text>
|
mb="md"
|
||||||
<Group style={{ color: 'black' }} pb={20}>
|
key={k}
|
||||||
|
withBorder
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="md"
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
>
|
||||||
|
<Title order={3}>{v.judul}</Title>
|
||||||
|
<Group style={{ color: 'black' }} pb="sm">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCalendar size={18} />
|
<IconCalendar size={18} />
|
||||||
<Text size="sm">
|
<Text
|
||||||
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', {
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
lh={{ base: 1.4, md: 1.5 }}
|
||||||
|
>
|
||||||
|
{v.createdAt
|
||||||
|
? new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}) : 'No date available'}
|
})
|
||||||
|
: 'No date available'}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Text ta={'justify'}>
|
<Text
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.6, md: 1.7 }}
|
||||||
|
>
|
||||||
{v.deskripsi}
|
{v.deskripsi}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
))
|
||||||
})}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Center,
|
Center,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
|
||||||
Grid,
|
Grid,
|
||||||
GridCol,
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
@@ -22,7 +21,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
UnstyledButton,
|
UnstyledButton
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
|
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
import { useTransitionRouter } from 'next-view-transitions';
|
||||||
@@ -98,10 +97,14 @@ function Page() {
|
|||||||
|
|
||||||
<Container size="lg" px="md">
|
<Container size="lg" px="md">
|
||||||
<Stack align="center" gap="0">
|
<Stack align="center" gap="0">
|
||||||
<Text fz={{ base: '2rem', md: '3.4rem' }} c={colors['blue-button']} fw="bold" ta="center">
|
<Title
|
||||||
|
order={1}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
ta="center"
|
||||||
|
>
|
||||||
Pengumuman Desa Darmasaba
|
Pengumuman Desa Darmasaba
|
||||||
</Text>
|
</Title>
|
||||||
<Text ta="center" px="md" pb={10}>
|
<Text ta="center" px="md" pb={10} fz={{ base: 'sm', md: 'md' }} lh="sm">
|
||||||
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
|
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -126,17 +129,17 @@ function Page() {
|
|||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
title={item.CategoryPengumuman?.name || 'Pengumuman'}
|
title={item.CategoryPengumuman?.name || 'Pengumuman'}
|
||||||
>
|
>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap="xs">
|
||||||
<Text fz="sm" fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
|
<Text fz={{ base: 'sm', md: 'sm' }} fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
|
||||||
{item.judul}
|
{item.judul}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta="justify" fz="sm" c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text ta="justify" fz={{ base: 'xs', md: 'sm' }} c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Flex pt={20} gap="md" justify="space-between">
|
<Group pt={20} gap="md" justify="space-between">
|
||||||
<Group style={{ color: 'black' }}>
|
<Group style={{ color: 'black' }}>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCalendar size={18} />
|
<IconCalendar size={18} />
|
||||||
<Text size="sm">
|
<Text fz={{ base: 'xs', md: 'sm' }}>
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -147,7 +150,7 @@ function Page() {
|
|||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconClock size={18} />
|
<IconClock size={18} />
|
||||||
<Text size="sm">
|
<Text fz={{ base: 'xs', md: 'sm' }}>
|
||||||
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@@ -157,11 +160,11 @@ function Page() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
||||||
<Text fs="unset" c={colors['blue-button']} fz="sm">
|
<Text fs="unset" c={colors['blue-button']} fz={{ base: 'xs', md: 'sm' }}>
|
||||||
Baca Selengkapnya
|
Baca Selengkapnya
|
||||||
</Text>
|
</Text>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Flex>
|
</Group>
|
||||||
</Notification>
|
</Notification>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -169,19 +172,19 @@ function Page() {
|
|||||||
|
|
||||||
<Paper p="md">
|
<Paper p="md">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text fw="bold" fz="lg" c={colors['blue-button']}>
|
<Title order={3} c={colors['blue-button']}>
|
||||||
Kategori
|
Kategori
|
||||||
</Text>
|
</Title>
|
||||||
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
|
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
|
||||||
const count = v._count?.pengumumans || 0;
|
const count = v._count?.pengumumans || 0;
|
||||||
return (
|
return (
|
||||||
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
|
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
|
||||||
<Paper bg={colors['BG-trans']} p={5}>
|
<Paper bg={colors['BG-trans']} p={5}>
|
||||||
<Group px={3} justify="space-between">
|
<Group px={3} justify="space-between">
|
||||||
<Text fz="md" c="black">
|
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||||
{v.name}
|
{v.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="md" c="black">
|
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||||
{count}
|
{count}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -200,7 +203,7 @@ function Page() {
|
|||||||
<Divider mb={10} color={colors['blue-button']} />
|
<Divider mb={10} color={colors['blue-button']} />
|
||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={{ base: 12, md: 8 }}>
|
<GridCol span={{ base: 12, md: 8 }}>
|
||||||
<Title order={3}>Daftar Pengumuman</Title>
|
<Title order={2}>Daftar Pengumuman</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 4 }}>
|
<GridCol span={{ base: 12, md: 4 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -210,6 +213,7 @@ function Page() {
|
|||||||
w="100%"
|
w="100%"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(e) => setSearchInput(e.target.value)}
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -223,7 +227,9 @@ function Page() {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : !state.findMany.data?.length ? (
|
) : !state.findMany.data?.length ? (
|
||||||
<Notification withCloseButton={false} h={100}>
|
<Notification withCloseButton={false} h={100}>
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} ta="center">
|
||||||
Tidak ada pengumuman yang ditemukan
|
Tidak ada pengumuman yang ditemukan
|
||||||
|
</Text>
|
||||||
</Notification>
|
</Notification>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
|
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
|
||||||
@@ -231,26 +237,26 @@ function Page() {
|
|||||||
<Paper key={item.id} p="md" withBorder radius="md" h="100%">
|
<Paper key={item.id} p="md" withBorder radius="md" h="100%">
|
||||||
<Stack h="100%" justify="space-between">
|
<Stack h="100%" justify="space-between">
|
||||||
<div>
|
<div>
|
||||||
<Text fw={600} c={colors['blue-button']} mb={5}>
|
<Text fw={600} c={colors['blue-button']} mb={5} fz={{ base: 'sm', md: 'md' }}>
|
||||||
{item.CategoryPengumuman?.name || 'Pengumuman'}
|
{item.CategoryPengumuman?.name || 'Pengumuman'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="lg" fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }}>
|
<Text fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }} fz={{ base: 'sm', md: 'lg' }}>
|
||||||
{item.judul}
|
{item.judul}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fz="sm"
|
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
lineClamp={4}
|
lineClamp={4}
|
||||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
mb="md"
|
mb="md"
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Group mb="sm" c="dimmed">
|
<Group mb="sm" c="dimmed">
|
||||||
<Group gap={5}>
|
<Group gap={5}>
|
||||||
<IconCalendar size={16} />
|
<IconCalendar size={16} />
|
||||||
<Text size="xs">
|
<Text fz={{ base: 'xs', md: 'xs' }}>
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -260,19 +266,19 @@ function Page() {
|
|||||||
</Group>
|
</Group>
|
||||||
<Group gap={5}>
|
<Group gap={5}>
|
||||||
<IconClock size={16} />
|
<IconClock size={16} />
|
||||||
<Text size="xs">
|
<Text fz={{ base: 'xs', md: 'xs' }}>
|
||||||
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
|
||||||
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
||||||
<Text fw={600} c={colors['blue-button']} size="sm">
|
<Text fw={600} c={colors['blue-button']} fz={{ base: 'sm', md: 'sm' }}>
|
||||||
Baca Selengkapnya →
|
Baca Selengkapnya →
|
||||||
</Text>
|
</Text>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -289,6 +295,7 @@ function Page() {
|
|||||||
siblings={1}
|
siblings={1}
|
||||||
boundaries={1}
|
boundaries={1}
|
||||||
withEdges
|
withEdges
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user