Compare commits
13 Commits
nico/5-des
...
nico/22-de
| Author | SHA1 | Date | |
|---|---|---|---|
| 29065cb3e2 | |||
| bf20cd55e8 | |||
| af60bcd6fc | |||
| dc8793e3ae | |||
| c8484357cb | |||
| 342e9bbc65 | |||
| f6f77d9e35 | |||
| a00481152c | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 |
@@ -1,14 +1,15 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
/* Mobile first */
|
||||
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -6,145 +7,207 @@ import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
nik: z.string().min(3, "NIK minimal 3 karakter"),
|
||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
||||
nik: z
|
||||
.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"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
jenisInformasiDimintaId: z.string().nonempty(),
|
||||
caraMemperolehInformasiId: z.string().nonempty(),
|
||||
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
||||
})
|
||||
});
|
||||
|
||||
const jenisInformasiDiminta = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load(){
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const caraMemperolehInformasi = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehInformasiGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const caraMemperolehSalinanInformasi = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(caraMemperolehSalinanInformasi)
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(caraMemperolehSalinanInformasi);
|
||||
|
||||
type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
type PermohonanInformasiPublikForm =
|
||||
Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
nik: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
email: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
name: true;
|
||||
nik: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
email: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
|
||||
const statepermohonanInformasiPublik = proxy({
|
||||
create: {
|
||||
form: {} as PermohonanInformasiPublikForm,
|
||||
loading: false,
|
||||
async create(){
|
||||
const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
|
||||
if(!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
statepermohonanInformasiPublik.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.load();
|
||||
return toast.success("Sukses menambahkan");
|
||||
}
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.create.loading = false;
|
||||
}
|
||||
create: {
|
||||
form: {} as PermohonanInformasiPublikForm,
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(
|
||||
statepermohonanInformasiPublik.create.form
|
||||
);
|
||||
|
||||
if (!cek.success) {
|
||||
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||
return false; // ⬅️ tambahkan return false
|
||||
}
|
||||
|
||||
try {
|
||||
statepermohonanInformasiPublik.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"create"
|
||||
].post(statepermohonanInformasiPublik.create.form);
|
||||
|
||||
if (res.data?.success === false) {
|
||||
toast.error(res.data?.message);
|
||||
return false; // ⬅️ gagal
|
||||
}
|
||||
|
||||
toast.success("Sukses menambahkan");
|
||||
return true; // ⬅️ sukses
|
||||
} catch {
|
||||
toast.error("Terjadi kesalahan server");
|
||||
return false;
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.create.loading = false;
|
||||
}
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PermohonanInformasiPublikGetPayload<{ include: {
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
} }>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
include: {
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true;
|
||||
jenisInformasiDiminta: true;
|
||||
caraMemperolehInformasi: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching program inovasi:", error);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
})
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
statepermohonanInformasiPublik.findMany.loading = true; // Use the full path to access the property
|
||||
statepermohonanInformasiPublik.findMany.page = page;
|
||||
statepermohonanInformasiPublik.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
|
||||
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
statepermohonanInformasiPublik.findMany.data = [];
|
||||
statepermohonanInformasiPublik.findMany.total = 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
statepermohonanInformasiPublik.findMany.data = [];
|
||||
statepermohonanInformasiPublik.findMany.total = 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
include: {
|
||||
jenisInformasiDiminta: true;
|
||||
caraMemperolehInformasi: true;
|
||||
caraMemperolehSalinanInformasi: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching program inovasi:", error);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statepermohonanInformasiPublikForm = proxy({
|
||||
statepermohonanInformasiPublik,
|
||||
jenisInformasiDiminta,
|
||||
caraMemperolehInformasi,
|
||||
caraMemperolehSalinanInformasi,
|
||||
})
|
||||
statepermohonanInformasiPublik,
|
||||
jenisInformasiDiminta,
|
||||
caraMemperolehInformasi,
|
||||
caraMemperolehSalinanInformasi,
|
||||
});
|
||||
|
||||
export default statepermohonanInformasiPublikForm;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -5,82 +6,130 @@ import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||
})
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email 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"),
|
||||
});
|
||||
|
||||
type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
type PermohonanKeberatanInformasiForm =
|
||||
Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
|
||||
const permohonanKeberatanInformasi = proxy({
|
||||
create: {
|
||||
form: {} as PermohonanKeberatanInformasiForm,
|
||||
loading: false,
|
||||
async create(){
|
||||
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
|
||||
if(!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
permohonanKeberatanInformasi.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.load();
|
||||
return toast.success("Sukses menambahkan");
|
||||
}
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
create: {
|
||||
form: {} as PermohonanKeberatanInformasiForm,
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(
|
||||
permohonanKeberatanInformasi.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||
return false; // ⬅️ tambahkan return false
|
||||
}
|
||||
try {
|
||||
permohonanKeberatanInformasi.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"create"
|
||||
].post(permohonanKeberatanInformasi.create.form);
|
||||
if (res.data?.success === false) {
|
||||
toast.error(res.data?.message);
|
||||
return false; // ⬅️ gagal
|
||||
}
|
||||
|
||||
toast.success("Sukses menambahkan");
|
||||
return true; // ⬅️ sukses
|
||||
} catch {
|
||||
toast.error("Terjadi kesalahan server");
|
||||
return false;
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
permohonanKeberatanInformasi.findMany.loading = true; // Use the full path to access the property
|
||||
permohonanKeberatanInformasi.findMany.page = page;
|
||||
permohonanKeberatanInformasi.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data.data || [];
|
||||
permohonanKeberatanInformasi.findMany.total = res.data.total || 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
permohonanKeberatanInformasi.findMany.data = [];
|
||||
permohonanKeberatanInformasi.findMany.total = 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findMany.data = [];
|
||||
permohonanKeberatanInformasi.findMany.total = 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/ppid/permohonankeberataninformasipublik/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to fetch permohonan keberatan informasi:",
|
||||
res.statusText
|
||||
);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default permohonanKeberatanInformasi;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
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 { IconBuildingStore, IconFileText, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -72,35 +72,76 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
))}
|
||||
</TabsList>
|
||||
</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) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -92,7 +92,7 @@ function EditKategoriBerita() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -43,7 +43,7 @@ function CreateKategoriBerita() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header dengan back button */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function KategoriBerita() {
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -48,6 +49,7 @@ function ListKategoriBerita({ search }: { search: string }) {
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,8 +60,8 @@ function ListKategoriBerita({ search }: { search: string }) {
|
||||
} = listDataState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
@@ -81,77 +83,84 @@ function ListKategoriBerita({ search }: { search: string }) {
|
||||
}
|
||||
|
||||
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 Kategori Berita</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/berita/kategori-berita/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Box py={{ base: 'sm', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kategori Berita
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/berita/kategori-berita/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||
<TableTh style={{ width: '50%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
|
||||
<TableTh w="50%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4} ta="center">Edit</Text>
|
||||
</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4} ta="center">Hapus</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="sm">{index + 1}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
<Text fz="sm" fw={500} lh={1.45} truncate="end">
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||
)
|
||||
}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</Button>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori berita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -161,22 +170,70 @@ function ListKategoriBerita({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs" mt="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder radius="md" p="sm" bg="white">
|
||||
<Box flex={1} ml="md">
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} truncate>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group mt="sm" justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="compact-xs"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="compact-xs"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={32}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori berita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
{totalPages > 1 && (
|
||||
<Center mt={{ base: 'lg', md: 'xl' }}>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
@@ -189,4 +246,4 @@ function ListKategoriBerita({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriBerita;
|
||||
export default KategoriBerita;
|
||||
@@ -1,8 +1,30 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabsBerita from './_com/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.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;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<LayoutTabsBerita>
|
||||
{children}
|
||||
|
||||
@@ -150,7 +150,7 @@ function EditBerita() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -41,7 +41,7 @@ function DetailBerita() {
|
||||
const data = beritaState.berita.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function CreateBerita() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header dengan tombol kembali */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,16 +45,17 @@ function Berita() {
|
||||
function ListBerita({ search }: { search: string }) {
|
||||
const beritaState = useProxy(stateDashboardBerita);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py="md">
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
@@ -63,64 +64,66 @@ function ListBerita({ search }: { search: string }) {
|
||||
const filteredData = data || [];
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Berita</Title>
|
||||
<Button
|
||||
leftSection={<IconCircleDashedPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconCircleDashedPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Judul</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh w="50%">Judul</TableTh>
|
||||
<TableTh w="30%">Kategori</TableTh>
|
||||
<TableTh w="20%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '30%' }}>
|
||||
<Box w={150}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={600} lh={1.45} truncate="end">
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{item.kategoriBerita?.name || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="sm"
|
||||
h={36}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<TableTd colSpan={3}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data berita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -130,6 +133,52 @@ function ListBerita({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="sm" mt="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="sm" fw={600} lh={1.4} c="dimmed">
|
||||
Judul
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
|
||||
<Text fz="sm" fw={600} lh={1.4} c="dimmed" mt="xs">
|
||||
Kategori
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.45} fw={500}>
|
||||
{item.kategoriBerita?.name || '-'}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
h={36}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data berita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
@@ -150,4 +199,4 @@ function ListBerita({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Berita;
|
||||
export default Berita;
|
||||
@@ -139,7 +139,7 @@ function EditFoto() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -45,7 +45,7 @@ function DetailFoto() {
|
||||
const imageUrl = data.imageGalleryFoto?.link;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -59,7 +59,7 @@ function DetailFoto() {
|
||||
withBorder
|
||||
// Gunakan max-width agar tidak terlalu lebar di desktop
|
||||
maw={800}
|
||||
w="100%"
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -72,7 +72,7 @@ function CreateFoto() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,6 +45,7 @@ function Foto() {
|
||||
function ListFoto({ search }: { search: string }) {
|
||||
const FotoState = useProxy(stateGallery.foto)
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -55,76 +56,81 @@ function ListFoto({ search }: { search: string }) {
|
||||
} = FotoState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
load(page, 10, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<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>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>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" }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<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>
|
||||
<TableTh>Judul Foto</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>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>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{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>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
truncate="end"
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<IconDeviceImac size={16} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -133,7 +139,7 @@ function ListFoto({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada foto yang cocok</Text>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>Tidak ada foto yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -141,7 +147,54 @@ function ListFoto({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder radius="sm" p="md">
|
||||
<Stack gap="xs">
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Judul Foto</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>{item.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImac size={16} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>Tidak ada foto yang cocok</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -160,4 +213,4 @@ function ListFoto({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Foto;
|
||||
export default Foto;
|
||||
@@ -1,7 +1,29 @@
|
||||
'use client'
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsGallery from "./lib/layoutTabs"
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
export default function Layout({ children }: { children: React.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;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<LayoutTabsGallery>
|
||||
{children}
|
||||
|
||||
@@ -118,7 +118,7 @@ function EditVideo() {
|
||||
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailVideo() {
|
||||
const data = videoState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -54,7 +54,7 @@ function DetailVideo() {
|
||||
{/* Detail Video */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -58,7 +58,7 @@ function CreateVideo() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,6 +45,7 @@ function Video() {
|
||||
function ListVideo({ search }: { search: string }) {
|
||||
const videoState = useProxy(stateGallery.video)
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -55,75 +56,77 @@ function ListVideo({ search }: { search: string }) {
|
||||
} = videoState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
load(page, 10, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={20}>
|
||||
<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 Video</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/gallery/video/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Box py={20}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Video
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/gallery/video/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover w="100%">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Judul Video</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh>Judul Video</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>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>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{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>
|
||||
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<IconDeviceImac size={18} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
@@ -132,8 +135,10 @@ function ListVideo({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada video yang cocok</Text>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada video yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -141,23 +146,74 @@ function ListVideo({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs" mt="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="sm" withBorder radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Judul Video</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Tanggal</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" lineClamp={5} fw={500} lh={1.45} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||
fz="sm"
|
||||
>
|
||||
<IconDeviceImac size={18} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada video yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</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>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Video;
|
||||
export default Video;
|
||||
@@ -115,7 +115,7 @@ function EditAjukanPermohonan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Back Button */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -48,7 +48,7 @@ function DetailAjukanPermohonan() {
|
||||
const data = ajukanPermohonanState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -61,7 +61,7 @@ function DetailAjukanPermohonan() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function AjukanPermohonan() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -44,6 +45,7 @@ function AjukanPermohonan() {
|
||||
function ListAjukanPermohonan({ search }: { search: string }) {
|
||||
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -54,58 +56,56 @@ function ListAjukanPermohonan({ search }: { search: string }) {
|
||||
} = AjukanPermohonanState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
// Loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Title order={4}>List Ajukan Permohonan</Title>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Title order={2} lh={1.2} mb={{ base: 'md', md: 'lg' }}>
|
||||
List Ajukan Permohonan
|
||||
</Title>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '45%' }}>Alamat</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>NIK</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Nama</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Alamat</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>NIK</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{data.length > 0 ? (
|
||||
data.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '30%' }}>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '45%' }}>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.alamat}
|
||||
</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.alamat}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '45%' }}>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.nik}
|
||||
</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.nik}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
@@ -123,9 +123,11 @@ function ListAjukanPermohonan({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data ajukan permohonan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -133,23 +135,71 @@ function ListAjukanPermohonan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="md">
|
||||
{data.length > 0 ? (
|
||||
data.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="md" shadow="xs">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.nama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.alamat}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>NIK</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.nik}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data ajukan permohonan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default AjukanPermohonan;
|
||||
export default AjukanPermohonan;
|
||||
@@ -1,10 +1,31 @@
|
||||
'use client'
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsLayanan from "../_com/layoutTabLayanan";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
export default function Layout({children} : {children: React.ReactNode}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/layanan/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/layanan/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/layanan/Pemerintahan/123 → panjang 6 → detail
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length >= 5;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<LayoutTabsLayanan>
|
||||
<Box>
|
||||
{children}
|
||||
</LayoutTabsLayanan>
|
||||
)
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabsLayanan>
|
||||
{children}
|
||||
</LayoutTabsLayanan>
|
||||
);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ function EditPelayananPendudukNonPermanent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Stack gap="xs">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -45,24 +45,28 @@ function PelayananPendudukNonPermanent() {
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
<Title
|
||||
order={3}
|
||||
lh={1.2}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Preview Pelayanan Penduduk Non Permanen
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -70,14 +74,14 @@ function PelayananPendudukNonPermanent() {
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||
<Center>
|
||||
<Text
|
||||
<Title
|
||||
order={2}
|
||||
lh={1.2}
|
||||
ta="center"
|
||||
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
{data.name}
|
||||
</Text>
|
||||
</Title>
|
||||
</Center>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
@@ -86,9 +90,11 @@ function PelayananPendudukNonPermanent() {
|
||||
<Text
|
||||
py={10}
|
||||
ta="justify"
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.55 }}
|
||||
c="dark"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -98,4 +104,4 @@ function PelayananPendudukNonPermanent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananPendudukNonPermanent;
|
||||
export default PelayananPendudukNonPermanent;
|
||||
@@ -123,7 +123,7 @@ function EditPelayananPerizinanBerusaha() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
|
||||
@@ -41,8 +41,7 @@ function PerizinanBerusaha() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// You should get the ID from your router query or params
|
||||
const id = 'edit'; // Replace with actual ID or get from URL params
|
||||
const id = 'edit';
|
||||
await pelayananPerizinanBerusaha.findById.load(id);
|
||||
} catch (err) {
|
||||
setError('Gagal memuat data');
|
||||
@@ -66,7 +65,7 @@ function PerizinanBerusaha() {
|
||||
if (error || !pelayananPerizinanBerusaha.findById.data) {
|
||||
return (
|
||||
<Center h={200}>
|
||||
<Text>{error || 'Data tidak ditemukan'}</Text>
|
||||
<Text c="dimmed">{error || 'Data tidak ditemukan'}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -79,24 +78,24 @@ function PerizinanBerusaha() {
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
<Title order={3} c={colors['blue-button']} lh={1.2}>
|
||||
Preview Pelayanan Perizinan Berusaha
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -104,38 +103,40 @@ function PerizinanBerusaha() {
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||
<Center>
|
||||
<Text
|
||||
<Title
|
||||
order={3}
|
||||
ta="center"
|
||||
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
lh={1.15}
|
||||
>
|
||||
{data.name}
|
||||
</Text>
|
||||
</Title>
|
||||
</Center>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
<Box mt="lg">
|
||||
<Text
|
||||
py={10}
|
||||
ta="justify"
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
py="xs"
|
||||
ta={{ base: "left", md: "justify" }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.55}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
py={10}
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
fw="bold"
|
||||
py="xs"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
>
|
||||
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
|
||||
umum:
|
||||
</Text>
|
||||
|
||||
<Box p="xl" w="100%">
|
||||
<Box p="xl" w="100%" visibleFrom='md'>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={setActive}
|
||||
@@ -143,28 +144,115 @@ function PerizinanBerusaha() {
|
||||
styles={{
|
||||
separator: { marginLeft: 25 },
|
||||
step: { padding: '12px 0' },
|
||||
stepLabel: {
|
||||
fontSize: 'var(--mantine-font-size-sm)',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
stepDescription: {
|
||||
fontSize: 'var(--mantine-font-size-xs)',
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
||||
Pendaftaran akun pada portal OSS
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Pendaftaran akun pada portal OSS
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
|
||||
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
|
||||
Memilih KBLI dengan jenis usaha yang akan didaftarkan
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Memilih KBLI dengan jenis usaha yang akan didaftarkan
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
|
||||
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
|
||||
Proses verifikasi dan persetujuan oleh instansi terkait
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Proses verifikasi dan persetujuan oleh instansi terkait
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
|
||||
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
|
||||
<Text fz="sm" lh={1.5}>
|
||||
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
|
||||
</Text>
|
||||
</StepperCompleted>
|
||||
</Stepper>
|
||||
|
||||
<Group justify="center" mt="xl">
|
||||
<Button variant="default" onClick={prevStep}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={nextStep}>Next step</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box p="xl" w="100%" hiddenFrom='md'>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={setActive}
|
||||
orientation="vertical"
|
||||
styles={{
|
||||
separator: { marginLeft: 25 },
|
||||
step: { padding: '12px 0' },
|
||||
stepLabel: {
|
||||
fontSize: 'var(--mantine-font-size-sm)',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
stepDescription: {
|
||||
fontSize: 'var(--mantine-font-size-xs)',
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
</Text>
|
||||
</StepperCompleted>
|
||||
</Stepper>
|
||||
|
||||
@@ -177,9 +265,10 @@ function PerizinanBerusaha() {
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
py={35}
|
||||
ta="justify"
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
py="md"
|
||||
ta={{ base: "left", md: "justify" }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.55}
|
||||
>
|
||||
Penting untuk diingat bahwa prosedur dan persyaratan dapat
|
||||
berubah seiring waktu. Untuk informasi yang lebih akurat dan
|
||||
@@ -203,5 +292,4 @@ function PerizinanBerusaha() {
|
||||
);
|
||||
}
|
||||
|
||||
export default PerizinanBerusaha;
|
||||
|
||||
export default PerizinanBerusaha;
|
||||
@@ -64,7 +64,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
@@ -49,7 +49,7 @@ function DetailSuratKeterangan() {
|
||||
const data = suratKeteranganState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -62,7 +62,7 @@ function DetailSuratKeterangan() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -75,20 +75,21 @@ function DetailSuratKeterangan() {
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="lg" fw="bold">
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="lg" fw="bold">
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text
|
||||
<Box pl={10}>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{
|
||||
@@ -96,9 +97,10 @@ function DetailSuratKeterangan() {
|
||||
}}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="lg" fw="bold">
|
||||
Gambar Konten Pelayanan
|
||||
</Text>
|
||||
@@ -117,7 +119,7 @@ function DetailSuratKeterangan() {
|
||||
Tidak ada gambar
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
|
||||
@@ -87,7 +87,7 @@ function CreateSuratKeterangan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -25,9 +25,10 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function SuratKeterangan() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
@@ -45,6 +46,7 @@ function SuratKeterangan() {
|
||||
function ListSuratKeterangan({ search }: { search: string }) {
|
||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -55,72 +57,80 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
||||
} = suratKeteranganState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
const keyword = search.toLowerCase();
|
||||
return data.filter(item =>
|
||||
item.name?.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi?.toLowerCase().includes(keyword)
|
||||
const keyword = debouncedSearch.toLowerCase();
|
||||
return data.filter(
|
||||
(item) =>
|
||||
item.name?.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi?.toLowerCase().includes(keyword)
|
||||
);
|
||||
}, [data, search]);
|
||||
}, [data, debouncedSearch]);
|
||||
|
||||
// Loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<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}>List Surat Keterangan</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
List Surat Keterangan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Nama
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Deskripsi
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '30%' }}>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end">
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '45%' }}>
|
||||
<Box w={200}>
|
||||
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '' }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
@@ -128,7 +138,9 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
@@ -139,8 +151,10 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" ta="center">
|
||||
Tidak ada data surat keterangan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -148,7 +162,67 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Box pl={8}>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.4}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '' }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
fullWidth
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" ta="center">
|
||||
Tidak ada data surat keterangan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -167,4 +241,4 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default SuratKeterangan;
|
||||
export default SuratKeterangan;
|
||||
@@ -74,13 +74,13 @@ function EditPelayananTelunjukSakti() {
|
||||
);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
deskripsi: originalData.deskripsi,
|
||||
link: originalData.link,
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
deskripsi: originalData.deskripsi,
|
||||
link: originalData.link,
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
// Submit: update global state hanya saat simpan
|
||||
const handleSubmit = async () => {
|
||||
@@ -102,12 +102,12 @@ function EditPelayananTelunjukSakti() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<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 Pelayanan Telunjuk Sakti Desa
|
||||
</Title>
|
||||
|
||||
@@ -50,7 +50,7 @@ function DetailPelayananTelunjukSakti() {
|
||||
const data = telunjukSaktiState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -63,7 +63,7 @@ function DetailPelayananTelunjukSakti() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -47,7 +47,7 @@ function CreatePelayananTelunjukDesa() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -26,9 +26,10 @@ import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function PelayananTelunjukSakti() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
@@ -46,46 +47,57 @@ function PelayananTelunjukSakti() {
|
||||
function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={400} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Pelayanan Telunjuk Sakti
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '40%' }}>Link</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Detail</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="30%">
|
||||
Nama
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="40%">
|
||||
Link
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="30%">
|
||||
Detail
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -93,18 +105,19 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text></Box>
|
||||
|
||||
<Text fz="sm" fw={500} lh={1.5} truncate="end">
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} style={{wordBreak: "break-word", whiteSpace: "normal"}} truncate="end" fz={"sm"} />
|
||||
</a>
|
||||
</Box>
|
||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
truncate="end"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</a>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -117,7 +130,9 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml="xs" fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -125,8 +140,8 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada data layanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -136,17 +151,68 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
<Stack gap="md">
|
||||
{filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="sm">
|
||||
<Box mb="xs">
|
||||
<Text fz='sm' fw={600} lh={1.4} c="gray.8">
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mb="xs">
|
||||
<Text fz='sm' fw={600} lh={1.4} c="gray.8">
|
||||
Link
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} component="a" href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
{item.deskripsi}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml="xs" fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada data layanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
}
|
||||
}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
@@ -155,5 +221,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananTelunjukSakti;
|
||||
|
||||
export default PelayananTelunjukSakti;
|
||||
@@ -133,7 +133,7 @@ function EditPenghargaan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Tombol Back + Title */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -49,7 +49,7 @@ function DetailPenghargaan() {
|
||||
const data = statePenghargaan.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
|
||||
@@ -73,7 +73,7 @@ function CreatePenghargaan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function Penghargaan() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -45,45 +46,48 @@ function Penghargaan() {
|
||||
function ListPenghargaan({ search }: { search: string }) {
|
||||
const state = useProxy(penghargaanState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
// Loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
<Stack py="md">
|
||||
<Skeleton h={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Title order={4}>List Penghargaan</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/penghargaan/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/penghargaan/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '35%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Aksi</TableTh>
|
||||
<TableTh w="35%">Nama</TableTh>
|
||||
<TableTh w="35%">Deskripsi</TableTh>
|
||||
<TableTh w="30%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -91,31 +95,27 @@ function ListPenghargaan({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text
|
||||
truncate="end"
|
||||
lineClamp={1}
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
lineClamp={1}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/penghargaan/${item.id}`)
|
||||
}
|
||||
@@ -127,9 +127,9 @@ function ListPenghargaan({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<TableTd colSpan={3}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data penghargaan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -139,7 +139,54 @@ function ListPenghargaan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack gap="sm" hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="md">
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mt="xs">
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text lineClamp={3} fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
<span dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Text>
|
||||
</Box>
|
||||
<Group mt="md">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/penghargaan/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data penghargaan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -148,7 +195,7 @@ function ListPenghargaan({ search }: { search: string }) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mt="lg"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
@@ -158,4 +205,4 @@ function ListPenghargaan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Penghargaan;
|
||||
export default Penghargaan;
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
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 { IconCategory, IconListDetails } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -54,35 +54,76 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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
|
||||
}}
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
<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) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -84,7 +84,7 @@ function EditKategoriPengumuman() {
|
||||
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -42,7 +42,7 @@ function CreateKategoriPengumuman() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header dengan back button */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -2,11 +2,21 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box, Button, Center,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Pagination,
|
||||
Paper, Skeleton, Stack,
|
||||
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
|
||||
Text, Title
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -15,6 +25,7 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function KategoriPengumuman() {
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -33,90 +44,134 @@ function KategoriPengumuman() {
|
||||
}
|
||||
|
||||
function ListKategoriPengumuman({ search }: { search: string }) {
|
||||
const listDataState = useProxy(stateDesaPengumuman.category)
|
||||
const listDataState = useProxy(stateDesaPengumuman.category);
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500);
|
||||
const { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(1, 10, search)
|
||||
}, [search])
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
listDataState.delete.delete(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
load(page, 10, search)
|
||||
listDataState.delete.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
load(page, 10, search);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Stack>
|
||||
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||
<Title order={4}>List Kategori Pengumuman</Title>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Stack gap={'lg'}>
|
||||
<Box
|
||||
visibleFrom="md"
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<Title order={4} lh={1.1}>
|
||||
List Kategori Pengumuman
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/pengumuman/kategori-pengumuman/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
<Title order={2} size="md" lh={1.1} ta="left">
|
||||
List Kategori Pengumuman
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/pengumuman/kategori-pengumuman/create')
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||
<TableTh w="60%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="15%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Edit
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="15%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Hapus
|
||||
</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate lineClamp={1}>{item.name}</Text>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant='light'
|
||||
color='green'
|
||||
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`
|
||||
)
|
||||
}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant='light'
|
||||
color='red'
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconTrash size={20} />
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -124,8 +179,10 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori pengumuman yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -133,6 +190,71 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Stack hiddenFrom="md" gap="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
bg={colors['white-1']}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`
|
||||
)
|
||||
}
|
||||
size="compact-xs"
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
size="compact-xs"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Paper withBorder p="xl" radius="md" bg={colors['white-1']}>
|
||||
<Center>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori pengumuman yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -153,7 +275,7 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
||||
text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?'
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriPengumuman;
|
||||
export default KategoriPengumuman;
|
||||
@@ -1,7 +1,29 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabs from './_com/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/pengumuman/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/pengumuman/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/pengumuman/Pemerintahan/123 → panjang 6 → detail
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length >= 5;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
|
||||
@@ -111,7 +111,7 @@ function EditPengumuman() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function DetailPengumuman() {
|
||||
const data = pengumumanState.pengumuman.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -61,7 +61,7 @@ export default function DetailPengumuman() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -74,14 +74,6 @@ export default function DetailPengumuman() {
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Kategori
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data?.CategoryPengumuman?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
@@ -92,6 +84,15 @@ export default function DetailPengumuman() {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Kategori
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data?.CategoryPengumuman?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Deskripsi
|
||||
|
||||
@@ -55,7 +55,7 @@ function CreatePengumuman() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -46,44 +46,56 @@ function Pengumuman() {
|
||||
function ListPengumuman({ search }: { search: string }) {
|
||||
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<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 Pengumuman</Title>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Pengumuman
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconCircleDashedPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '40%' }}>Judul</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Detail</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Judul
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Kategori
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left">
|
||||
Detail
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -91,14 +103,12 @@ function ListPengumuman({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.CategoryPengumuman?.name || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -109,9 +119,12 @@ function ListPengumuman({ search }: { search: string }) {
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="sm"
|
||||
py="xs"
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -119,8 +132,10 @@ function ListPengumuman({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text>
|
||||
<Center py={{ base: 'sm', md: 'md' }}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada pengumuman yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -128,7 +143,59 @@ function ListPengumuman({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card List */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="sm">
|
||||
<Stack gap="xs">
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Judul
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Kategori
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.CategoryPengumuman?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
|
||||
}
|
||||
fullWidth
|
||||
fz="sm"
|
||||
mt="xs"
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="sm">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada pengumuman yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -147,4 +214,4 @@ function ListPengumuman({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Pengumuman;
|
||||
export default Pengumuman;
|
||||
@@ -93,7 +93,7 @@ function EditKategoriPotensi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -41,7 +41,7 @@ function CreateKategoriPotensi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header dengan back button */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -9,6 +26,7 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import potensiDesaState from '../../../_state/desa/potensi';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function KategoriPotensi() {
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -27,85 +45,110 @@ function KategoriPotensi() {
|
||||
}
|
||||
|
||||
function ListKategoriPotensi({ search }: { search: string }) {
|
||||
const listDataState = useProxy(potensiDesaState.kategoriPotensi)
|
||||
const listDataState = useProxy(potensiDesaState.kategoriPotensi);
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
load(1, 10, search)
|
||||
}, [search])
|
||||
load(1, 10, debouncedSearch);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
listDataState.delete.delete(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
load(page, 10, search)
|
||||
listDataState.delete.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
load(page, 10, search);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py="md">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>List Kategori Potensi</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Stack gap="xl">
|
||||
<Group justify="space-between" align="center">
|
||||
<Title order={4} lh={1.2}>
|
||||
List Kategori Potensi
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/potensi/kategori-potensi/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover striped withRowBorders miw={700}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||
<TableTh w="60%">
|
||||
<Text fz="xs" fw={600} lh={1.4} c="black">
|
||||
Nama
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="15%">
|
||||
<Text fz="xs" fw={600} lh={1.4} c="black">
|
||||
Edit
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="15%">
|
||||
<Text fz="xs" fw={600} lh={1.4} c="black">
|
||||
Hapus
|
||||
</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||
<Text fz="sm" lh={1.5} truncate>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate lineClamp={1}>{item.nama}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/potensi/kategori-potensi/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant='light'
|
||||
color='red'
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
@@ -114,8 +157,10 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text>
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada data kategori potensi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -123,10 +168,70 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack gap="sm" hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
No
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{(page - 1) * 10 + index + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/potensi/kategori-potensi/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="xs"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada data kategori potensi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center mt="md">
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage, 10, search)}
|
||||
@@ -143,7 +248,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
||||
text='Apakah anda yakin ingin menghapus kategori Potensi ini?'
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriPotensi;
|
||||
export default KategoriPotensi;
|
||||
@@ -1,8 +1,29 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabsPotensi from './_lib/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.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;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabsPotensi>
|
||||
{children}
|
||||
|
||||
@@ -143,7 +143,7 @@ function EditPotensi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function DetailPotensi() {
|
||||
const data = potensiState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -52,7 +52,7 @@ export default function DetailPotensi() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -79,7 +79,7 @@ function CreatePotensi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import potensiDesaState from '../../../_state/desa/potensi';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function Potensi() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -46,6 +47,7 @@ function Potensi() {
|
||||
function ListPotensi({ search }: { search: string }) {
|
||||
const potensiState = useProxy(potensiDesaState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -57,41 +59,61 @@ function ListPotensi({ search }: { search: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
potensiState.kategoriPotensi.findMany.load();
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py="lg">
|
||||
<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 Potensi Desa</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Box py="lg">
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Potensi Desa
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={700}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '20%' }}>Judul</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Detail</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Judul
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Kategori
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="35%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="15%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -99,27 +121,23 @@ function ListPotensi({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
|
||||
</Box>
|
||||
<Text fz="sm" c="gray.7" lh={1.5}>
|
||||
{item.kategori?.nama || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={300}>
|
||||
<Text
|
||||
lineClamp={1}
|
||||
truncate
|
||||
fz="sm"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
lineClamp={2}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -138,8 +156,10 @@ function ListPotensi({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
|
||||
<Center py="xl">
|
||||
<Text c="gray.6" fz="sm" ta="center" lh={1.5}>
|
||||
Tidak ada data potensi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -147,7 +167,64 @@ function ListPotensi({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Box mb="xs">
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Judul
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mb="xs">
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Kategori
|
||||
</Text>
|
||||
<Text fz="sm" c="gray.7" lh={1.5}>
|
||||
{item.kategori?.nama || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mb="xs">
|
||||
<Text fz="xs" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
|
||||
w="100%"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="gray.6" fz="sm" ta="center" lh={1.5}>
|
||||
Tidak ada data potensi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -166,4 +243,4 @@ function ListPotensi({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Potensi;
|
||||
export default Potensi;
|
||||
154
src/app/admin/(dashboard)/desa/profil/_lib/layoutTabsDetail.tsx
Normal file
154
src/app/admin/(dashboard)/desa/profil/_lib/layoutTabsDetail.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { IconCalendar, IconUser, IconUsers } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Profil Desa",
|
||||
value: "profildesa",
|
||||
href: "/admin/desa/profil/profil-desa",
|
||||
icon: <IconUser size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profil Perbekel",
|
||||
value: "profilperbekel",
|
||||
href: "/admin/desa/profil/profil-perbekel",
|
||||
icon: <IconUsers size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profil Perbekel Dari Masa Ke Masa",
|
||||
value: "profilperbekeldarimasakemasa",
|
||||
href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
|
||||
icon: <IconCalendar size={18} stroke={1.8} />
|
||||
}
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
||||
<Tabs
|
||||
color={colors["blue-button"]}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</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) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||
<>{children}</>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsDetail;
|
||||
@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
|
||||
{
|
||||
label: "Sejarah Desa",
|
||||
value: "sejarahdesa",
|
||||
href: "/admin/desa/profile/edit/sejarah_desa"
|
||||
href: "/admin/desa/profil/edit/sejarah_desa"
|
||||
},
|
||||
{
|
||||
label: "Visi Misi Desa",
|
||||
value: "visimisidesa",
|
||||
href: "/admin/desa/profile/edit/visi_misi_desa"
|
||||
href: "/admin/desa/profil/edit/visi_misi_desa"
|
||||
},
|
||||
{
|
||||
label: "Lambang Desa",
|
||||
value: "lambangdesa",
|
||||
href: "/admin/desa/profile/edit/lambang_desa"
|
||||
href: "/admin/desa/profil/edit/lambang_desa"
|
||||
},
|
||||
{
|
||||
label: "Maskot Desa",
|
||||
value: "maskotdesa",
|
||||
href: "/admin/desa/profile/edit/maskot_desa"
|
||||
href: "/admin/desa/profil/edit/maskot_desa"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
33
src/app/admin/(dashboard)/desa/profil/layout.tsx
Normal file
33
src/app/admin/(dashboard)/desa/profil/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsDetail from "./_lib/layoutTabsDetail"
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
export default function Layout({ children }: { children: React.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;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<LayoutTabsDetail>
|
||||
{children}
|
||||
</LayoutTabsDetail>
|
||||
)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -148,7 +148,7 @@ function Page() {
|
||||
// ❌ Error
|
||||
if (loadError) {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
@@ -156,7 +156,7 @@ function Page() {
|
||||
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
||||
{loadError}
|
||||
</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
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -166,7 +166,7 @@ function Page() {
|
||||
|
||||
// 🧱 UI utama
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Group mb="sm">
|
||||
@@ -40,7 +40,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/desa/profile/profile-desa");
|
||||
router.push("/admin/desa/profil/profil-desa");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success("Maskot berhasil diperbarui!");
|
||||
router.push("/admin/desa/profile/profile-desa");
|
||||
router.push("/admin/desa/profil/profil-desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error update maskot:", error);
|
||||
@@ -170,7 +170,7 @@ function Page() {
|
||||
// Loading state
|
||||
if (maskotState.findUnique.loading || maskotState.update.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Center h={400}>
|
||||
<Text>Memuat data...</Text>
|
||||
</Center>
|
||||
@@ -181,7 +181,7 @@ function Page() {
|
||||
// Error state
|
||||
if (maskotState.findUnique.error) {
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
@@ -196,7 +196,7 @@ function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
@@ -50,7 +50,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ function Page() {
|
||||
}, [params?.id, router]);
|
||||
|
||||
// 🔄 Check if form has changes
|
||||
|
||||
|
||||
|
||||
// 🔁 Reset Form to Original Data
|
||||
const handleResetForm = () => {
|
||||
@@ -122,7 +122,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -149,7 +149,7 @@ function Page() {
|
||||
// 🔄 Loading State
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Center h={400}>
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" color={colors['blue-button']} />
|
||||
@@ -165,7 +165,7 @@ function Page() {
|
||||
// ❌ Error State
|
||||
if (loadError) {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
@@ -179,7 +179,7 @@ function Page() {
|
||||
{loadError}
|
||||
</Alert>
|
||||
<Button
|
||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
||||
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Halaman Utama
|
||||
@@ -42,7 +42,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -126,7 +126,7 @@ function Page() {
|
||||
// ⏳ Loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Center h={400}>
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" color={colors['blue-button']} />
|
||||
@@ -142,7 +142,7 @@ function Page() {
|
||||
// ❌ Error
|
||||
if (loadError) {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
@@ -156,7 +156,7 @@ function Page() {
|
||||
{loadError}
|
||||
</Alert>
|
||||
<Button
|
||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
||||
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Halaman Utama
|
||||
590
src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx
Normal file
590
src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
'use client'
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Card, Center, Divider, Group, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import stateProfileDesa from '../../../_state/desa/profile';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter();
|
||||
const snap = useSnapshot(stateProfileDesa);
|
||||
|
||||
useEffect(() => {
|
||||
stateProfileDesa.sejarahDesa.findUnique.load("edit");
|
||||
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
|
||||
stateProfileDesa.lambangDesa.findUnique.load("edit");
|
||||
stateProfileDesa.maskotDesa.findUnique.load("edit");
|
||||
}, []);
|
||||
|
||||
const sejarah = snap.sejarahDesa.findUnique.data;
|
||||
const visiMisi = snap.visiMisiDesa.findUnique.data;
|
||||
const lambang = snap.lambangDesa.findUnique.data;
|
||||
const maskot = snap.maskotDesa.findUnique.data;
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="lg">
|
||||
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
|
||||
|
||||
{/* Sejarah Desa */}
|
||||
|
||||
<Box visibleFrom='md'>
|
||||
{sejarah && (
|
||||
<Paper p={{ base: "lg", md: "xl" }} bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Group justify='space-between'>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{sejarah.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "md", md: "h3" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
{sejarah && (
|
||||
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Preview Sejarah Desa
|
||||
</Title>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
radius="md"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content Wrapper */}
|
||||
<Box
|
||||
mx="auto"
|
||||
w="100%"
|
||||
maw={720} // batas nyaman baca
|
||||
>
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
p={{ base: "md", md: "lg" }}
|
||||
>
|
||||
{/* Logo + Title */}
|
||||
<Stack align="center" gap="xs">
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
alt="Logo Desa"
|
||||
w={{ base: 120, md: 200 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="md"
|
||||
py="sm"
|
||||
radius="md"
|
||||
mt={{ base: "sm", md: -24 }} // aman di mobile
|
||||
>
|
||||
<Text
|
||||
ta="center"
|
||||
c="white"
|
||||
fw={700}
|
||||
fz={{ base: "lg", md: "xl" }}
|
||||
>
|
||||
{sejarah.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box pl={10}>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh="1.6"
|
||||
ta="left"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Visi Misi Desa */}
|
||||
<Box visibleFrom='md'>
|
||||
{visiMisi && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Group justify='space-between'>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
Visi Misi Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Box px={20}>
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
|
||||
</Box>
|
||||
<Box px={20}>
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
{visiMisi && (
|
||||
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Preview Visi Misi Desa
|
||||
</Title>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
radius="md"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content Wrapper */}
|
||||
<Box
|
||||
mx="auto"
|
||||
w="100%"
|
||||
maw={720} // batas nyaman baca
|
||||
>
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
p={{ base: "md", md: "lg" }}
|
||||
>
|
||||
{/* Logo + Title */}
|
||||
<Stack align="center" gap="xs">
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
alt="Logo Desa"
|
||||
w={{ base: 120, md: 200 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="md"
|
||||
py="sm"
|
||||
radius="md"
|
||||
mt={{ base: "sm", md: -24 }} // aman di mobile
|
||||
>
|
||||
<Text
|
||||
ta="center"
|
||||
c="white"
|
||||
fw={700}
|
||||
fz={{ base: "lg", md: "xl" }}
|
||||
>
|
||||
Visi Misi Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
<Stack pl={10}>
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh="1.6"
|
||||
ta="left"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
dangerouslySetInnerHTML={{ __html: visiMisi.visi }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack pl={10}>
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh="1.6"
|
||||
ta="left"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
dangerouslySetInnerHTML={{ __html: visiMisi.misi }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Lambang Desa */}
|
||||
<Box visibleFrom='md'>
|
||||
{lambang && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Group justify='space-between'>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{lambang.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
{lambang && (
|
||||
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Preview Lambang Desa
|
||||
</Title>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
radius="md"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content Wrapper */}
|
||||
<Box
|
||||
mx="auto"
|
||||
w="100%"
|
||||
maw={720} // batas nyaman baca
|
||||
>
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
p={{ base: "md", md: "lg" }}
|
||||
>
|
||||
{/* Logo + Title */}
|
||||
<Stack align="center" gap="xs">
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
alt="Logo Desa"
|
||||
w={{ base: 120, md: 200 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="md"
|
||||
py="sm"
|
||||
radius="md"
|
||||
mt={{ base: "sm", md: -24 }} // aman di mobile
|
||||
>
|
||||
<Text
|
||||
ta="center"
|
||||
c="white"
|
||||
fw={700}
|
||||
fz={{ base: "lg", md: "xl" }}
|
||||
>
|
||||
{lambang.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box pl={10}>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh="1.6"
|
||||
ta="left"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
dangerouslySetInnerHTML={{ __html: lambang.deskripsi }}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Maskot Desa */}
|
||||
<Box visibleFrom='md'>
|
||||
{maskot && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Group justify='space-between'>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
Maskot Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
|
||||
</Box>
|
||||
<Stack mt="md" gap="sm">
|
||||
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
|
||||
{maskot.images.map((img, idx) => (
|
||||
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
|
||||
<Center>
|
||||
<Image
|
||||
src={img.image.link}
|
||||
alt={img.label}
|
||||
w={150}
|
||||
h={150}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ border: '1px solid #ccc' }}
|
||||
loading='lazy'
|
||||
/>
|
||||
</Center>
|
||||
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
{maskot && (
|
||||
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Preview Maskot Desa
|
||||
</Title>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
radius="md"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content Wrapper */}
|
||||
<Box
|
||||
mx="auto"
|
||||
w="100%"
|
||||
maw={720} // batas nyaman baca
|
||||
>
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
p={{ base: "md", md: "lg" }}
|
||||
>
|
||||
{/* Logo + Title */}
|
||||
<Stack align="center" gap="xs">
|
||||
<Image
|
||||
src="/pudak-icon.png"
|
||||
alt="Logo Desa"
|
||||
w={{ base: 120, md: 200 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="md"
|
||||
py="sm"
|
||||
radius="md"
|
||||
mt={{ base: "sm", md: -24 }} // aman di mobile
|
||||
>
|
||||
<Text
|
||||
ta="center"
|
||||
c="white"
|
||||
fw={700}
|
||||
fz={{ base: "lg", md: "xl" }}
|
||||
>
|
||||
Maskot Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box pl={10}>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh="1.6"
|
||||
ta="left"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
dangerouslySetInnerHTML={{ __html: maskot.deskripsi }}
|
||||
/>
|
||||
</Box>
|
||||
<Stack mt="md" gap="sm">
|
||||
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
|
||||
{maskot.images.map((img, idx) => (
|
||||
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
|
||||
<Center>
|
||||
<Image
|
||||
src={img.image.link}
|
||||
alt={img.label}
|
||||
w={150}
|
||||
h={150}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ border: '1px solid #ccc' }}
|
||||
loading='lazy'
|
||||
/>
|
||||
</Center>
|
||||
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
|
||||
|
||||
await state.update.update();
|
||||
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) {
|
||||
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||
@@ -127,7 +127,7 @@ function EditPerbekelDariMasaKeMasa() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
@@ -4,7 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
|
||||
state.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
||||
router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailPerbekelDariMasa() {
|
||||
const data = state.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -52,7 +52,7 @@ function DetailPerbekelDariMasa() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -108,12 +108,12 @@ function DetailPerbekelDariMasa() {
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
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"
|
||||
radius="md"
|
||||
size="md"
|
||||
@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
|
||||
state.create.form.imageId = uploaded.id;
|
||||
await state.create.create();
|
||||
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) {
|
||||
console.error(error);
|
||||
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
||||
@@ -56,7 +56,7 @@ function CreatePerbekelDariMasaKeMasa() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Back button + Title */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateProfileDesa from '../../../_state/desa/profile';
|
||||
|
||||
function PerbekelDariMasaKeMasa() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Perbekel Dari Masa Ke Masa'
|
||||
placeholder='Cari nama perbekel...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPerbekelDariMasaKeMasa search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
||||
const router = useRouter();
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch)
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify='space-between' mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
List Perbekel Dari Masa Ke Masa
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="dark.9">Nama Perbekel</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="dark.9">Periode</TableTh>
|
||||
<TableTh fz="sm" fw={600} ta="left" c="dark.9">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>{item.nama}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="md" lh={1.5} lineClamp={1}>{item.periode}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={{ base: 'md', md: 'lg' }}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data perbekel yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark.9">Nama Perbekel</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.nama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark.9">Periode</Text>
|
||||
<Text fz="sm" lh={1.5}>{item.periode}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||
fullWidth
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="md">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data perbekel yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt={{ base: 'sm', md: 'md' }}
|
||||
mb={{ base: 'sm', md: 'md' }}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PerbekelDariMasaKeMasa;
|
||||
@@ -25,7 +25,7 @@ function ProfilePerbekel() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/desa/profile/profile-perbekel");
|
||||
router.push("/admin/desa/profil/profil-perbekel");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
|
||||
const success = await perbekelState.edit.submit()
|
||||
if (success) {
|
||||
toast.success("Data berhasil disimpan");
|
||||
router.push("/admin/desa/profile/profile-perbekel");
|
||||
router.push("/admin/desa/profil/profil-perbekel");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error update sejarah desa:", error);
|
||||
@@ -97,7 +97,7 @@ function ProfilePerbekel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
@@ -33,18 +33,18 @@ function Page() {
|
||||
{/* Header + tombol edit */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||
<Title order={2} c={colors['blue-button']} lh={1.2} />
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -58,7 +58,13 @@ function Page() {
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={12}>
|
||||
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
lh={{ base: 1.45, md: 1.45 }}
|
||||
>
|
||||
Profil Pimpinan Badan Publik Desa Darmasaba
|
||||
</Text>
|
||||
</GridCol>
|
||||
@@ -86,25 +92,55 @@ function Page() {
|
||||
className="glass3"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
I.B. Surya Prabhawa Manuaba, S.H., M.H.
|
||||
<Text
|
||||
ta="center"
|
||||
c={colors['white-1']}
|
||||
fw="bolder"
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
>
|
||||
I.B. Surya Prabhawa Manuaba, S.H., M.H.
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
{/* Biodata & Info */}
|
||||
<Box mt="lg">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
|
||||
<Title order={3} lh={1.2} mb={4} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.6 }}
|
||||
ta="justify"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: perbekel.biodata }}
|
||||
/>
|
||||
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
|
||||
<Title order={3} lh={1.2} mt="md" mb={4} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.6 }}
|
||||
ta="justify"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }}
|
||||
/>
|
||||
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman Organisasi</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
|
||||
<Title order={3} lh={1.2} mt="md" mb={4} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.6 }}
|
||||
ta="justify"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }}
|
||||
/>
|
||||
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Program Kerja Unggulan</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
|
||||
<Title order={3} lh={1.2} mt="md" mb={4} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.6 }}
|
||||
ta="justify"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -112,4 +148,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,113 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { IconCalendar, IconUser, IconUsers } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Profile Desa",
|
||||
value: "profiledesa",
|
||||
href: "/admin/desa/profile/profile-desa",
|
||||
icon: <IconUser size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profile Perbekel",
|
||||
value: "profileperbekel",
|
||||
href: "/admin/desa/profile/profile-perbekel",
|
||||
icon: <IconUsers size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||
value: "profile-perbekel-dari-masa-ke-masa",
|
||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
||||
icon: <IconCalendar size={18} stroke={1.8} />
|
||||
}
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
||||
<Tabs
|
||||
color={colors["blue-button"]}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||
<>{children}</>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsDetail;
|
||||
@@ -1,11 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import LayoutTabsDetail from "./_lib/layoutTabsDetail"
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabsDetail>
|
||||
{children}
|
||||
</LayoutTabsDetail>
|
||||
)
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import stateProfileDesa from '../../../_state/desa/profile';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter();
|
||||
const snap = useSnapshot(stateProfileDesa);
|
||||
|
||||
useEffect(() => {
|
||||
stateProfileDesa.sejarahDesa.findUnique.load("edit");
|
||||
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
|
||||
stateProfileDesa.lambangDesa.findUnique.load("edit");
|
||||
stateProfileDesa.maskotDesa.findUnique.load("edit");
|
||||
}, []);
|
||||
|
||||
const sejarah = snap.sejarahDesa.findUnique.data;
|
||||
const visiMisi = snap.visiMisiDesa.findUnique.data;
|
||||
const lambang = snap.lambangDesa.findUnique.data;
|
||||
const maskot = snap.maskotDesa.findUnique.data;
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="lg">
|
||||
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
|
||||
|
||||
{/* Sejarah Desa */}
|
||||
{sejarah && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{sejarah.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Text fz={{ base: "md", md: "h3" }} style={{wordBreak: "break-word", whiteSpace: "normal"}} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Visi Misi Desa */}
|
||||
{visiMisi && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
Visi Misi Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
|
||||
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Lambang Desa */}
|
||||
{lambang && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{lambang.judul}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Maskot Desa */}
|
||||
{maskot && (
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
Maskot Desa
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
|
||||
<Stack mt="md" gap="sm">
|
||||
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
|
||||
{maskot.images.map((img, idx) => (
|
||||
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
|
||||
<Center>
|
||||
<Image
|
||||
src={img.image.link}
|
||||
alt={img.label}
|
||||
w={150}
|
||||
h={150}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ border: '1px solid #ccc' }}
|
||||
loading='lazy'
|
||||
/>
|
||||
</Center>
|
||||
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -1,133 +0,0 @@
|
||||
'use client'
|
||||
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 { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateProfileDesa from '../../../_state/desa/profile';
|
||||
|
||||
function PerbekelDariMasaKeMasa() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Perbekel Dari Masa Ke Masa'
|
||||
placeholder='Cari nama perbekel...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPerbekelDariMasaKeMasa search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
||||
const router = useRouter();
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} 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}>List Perbekel Dari Masa Ke Masa</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '35%' }}>Nama Perbekel</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Periode</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} lineClamp={1}>{item.nama}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text lineClamp={1}>{item.periode}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data perbekel 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 PerbekelDariMasaKeMasa;
|
||||
@@ -142,7 +142,7 @@ function EditKeamananLingkungan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -44,7 +44,7 @@ function DetailKeamananLingkungan() {
|
||||
const data = keamananState.findUnique.data
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -58,7 +58,7 @@ function DetailKeamananLingkungan() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -88,7 +88,9 @@ function DetailKeamananLingkungan() {
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
<Box pl={8}>
|
||||
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Aksi */}
|
||||
|
||||
@@ -78,7 +78,7 @@ function CreateKeamananLingkungan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -48,6 +48,7 @@ function KeamananLingkungan() {
|
||||
function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
const keamananState = useProxy(keamananLingkunganState)
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000)
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,25 +59,27 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
} = keamananState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
load(page, 10, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Keamanan Lingkungan</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Keamanan Lingkungan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -89,14 +92,18 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover
|
||||
miw={0}
|
||||
style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: 140, textAlign: 'center' }}>
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -104,25 +111,32 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<TableTd style={{ overflow: 'hidden' }}>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lh={1.5}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: 140, textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
|
||||
leftSection={<IconDeviceImacCog size={20} />}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -130,8 +144,8 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Center py={{ base: 'sm', md: 'lg' }}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data keamanan lingkungan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -141,6 +155,49 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder radius="md" p="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Box pl={8}>
|
||||
<Text fz="sm" lineClamp={3} style={{ wordBreak: 'break-word' }} fw={500} lh={1.5} c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data keamanan lingkungan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -152,8 +209,8 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
mt={{ base: 'sm', md: 'md' }}
|
||||
mb={{ base: 'sm', md: 'md' }}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
@@ -162,4 +219,4 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KeamananLingkungan;
|
||||
export default KeamananLingkungan;
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
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 { IconPhone, IconTag } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -57,35 +57,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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
|
||||
}}
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
<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) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -107,7 +107,7 @@ function EditKontakItem() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -41,7 +41,7 @@ function DetailKontakDarurat() {
|
||||
const data = kontakState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -55,7 +55,7 @@ function DetailKontakDarurat() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -46,7 +46,7 @@ function CreateKontakItem() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -26,7 +26,6 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import kontakDarurat from '../../../_state/keamanan/kontak-darurat-keamanan';
|
||||
|
||||
|
||||
function KontakItem() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
@@ -49,6 +48,7 @@ function KontakItem() {
|
||||
function ListKontakItem({ search }: { search: string }) {
|
||||
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -59,43 +59,54 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
} = kontakState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kontak Darurat Item</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kontak Darurat Item
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item/create')}
|
||||
onClick={() =>
|
||||
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop: Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kontak</TableTh>
|
||||
<TableTh>Nomor Telepon</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '45%' }}>Nama Kontak</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Nomor Telepon</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -103,12 +114,12 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end">
|
||||
{item.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="dimmed" lh={1.5}>
|
||||
{item.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -116,19 +127,26 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`
|
||||
)
|
||||
}
|
||||
size="xs"
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<IconDeviceImacCog size={16} />
|
||||
<Text ml={4} fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat item yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -138,6 +156,58 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile: Card */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="sm" withBorder radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama Kontak
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nomor Telepon
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4} c="dimmed">
|
||||
{item.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={16} />
|
||||
<Text ml={4} fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat item yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -159,4 +229,4 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KontakItem;
|
||||
export default KontakItem;
|
||||
@@ -147,7 +147,7 @@ export default function EditKontakDaruratKeamanan() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
|
||||
@@ -42,7 +42,7 @@ function DetailKontakDaruratKeamanan() {
|
||||
const data = kontakState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -56,7 +56,7 @@ function DetailKontakDaruratKeamanan() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -53,7 +53,7 @@ function CreateKontakDaruratKeamanan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -48,6 +48,7 @@ function KontakDaruratKeamanan() {
|
||||
function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,26 +59,28 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
} = kontakState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
load(page, 10, debouncedSearch);
|
||||
kontakDarurat.kontakDaruratItem.findMany.load();
|
||||
}, [page, search]);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kontak Darurat Keamanan</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kontak Darurat Keamanan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -88,15 +91,22 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Kontak Darurat</TableTh>
|
||||
<TableTh>Nama Kontak</TableTh>
|
||||
<TableTh>Nomor Telepon</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Kontak Darurat</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama Kontak</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nomor Telepon</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -104,17 +114,17 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" truncate lineClamp={1}>
|
||||
<Text fz="sm" c="dimmed" lh={1.5} truncate lineClamp={1}>
|
||||
{item.kategori?.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="dimmed" lh={1.5}>
|
||||
{item.kategori?.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -125,7 +135,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -134,7 +144,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat keamanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -144,6 +154,55 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder radius="md" p="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kontak Darurat</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama Kontak</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.kategori?.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.kategori?.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat keamanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -165,4 +224,4 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KontakDaruratKeamanan;
|
||||
export default KontakDaruratKeamanan;
|
||||
@@ -1,8 +1,29 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabs from './_lib/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.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;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
|
||||
@@ -127,7 +127,7 @@ function EditLaporanPublik() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -48,7 +48,7 @@ function DetailLaporanPublik() {
|
||||
const data = stateLaporan.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -61,7 +61,7 @@ function DetailLaporanPublik() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -50,7 +50,7 @@ function CreateLaporanPublik() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header with Back Button */}
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,6 +45,7 @@ function LaporanPublik() {
|
||||
function ListLaporanPublik({ search }: { search: string }) {
|
||||
const stateLaporan = useProxy(laporanPublikState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -55,42 +56,54 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
} = stateLaporan.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<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 Laporan Publik</Title>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Laporan Publik
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
|
||||
visibleFrom="md"
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Judul Laporan Publik</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Status</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Judul Laporan Publik</Text></TableTh>
|
||||
<TableTh style={{ width: '20%' }}><Text fz="sm" fw={600} lh={1.4}>Tanggal</Text></TableTh>
|
||||
<TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Status</Text></TableTh>
|
||||
<TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text></TableTh>
|
||||
<TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -98,12 +111,12 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
<Text fw={500} fz="md" lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="gray.7" lh={1.5}>
|
||||
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -131,9 +144,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</Box>
|
||||
<Text truncate fz="sm" c="gray.7" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -144,7 +155,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -153,7 +164,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="gray.6" fz="sm" lh={1.5}>
|
||||
Tidak ada data laporan publik yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -163,7 +174,88 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Judul Laporan Publik</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.judul}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="gray.7">
|
||||
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
|
||||
<Box
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '16px',
|
||||
backgroundColor:
|
||||
item.status === 'Selesai' ? '#94EF95FF' :
|
||||
item.status === 'Proses' ? '#F1D295FF' :
|
||||
'#F38E8EFF',
|
||||
color:
|
||||
item.status === 'Selesai' ? '#01BA01FF' :
|
||||
item.status === 'Proses' ? '#B67A00FF' :
|
||||
'#AE1700FF',
|
||||
fontWeight: 900,
|
||||
fontSize: '0.75rem',
|
||||
textAlign: 'center',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
>
|
||||
{item.status}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="gray.7" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
router.push(`/admin/keamanan/laporan-publik/${item.id}`)
|
||||
}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="gray.6" fz="sm" lh={1.5}>
|
||||
Tidak ada data laporan publik yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Tambah Baru (Mobile only) */}
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
|
||||
hiddenFrom="md"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -182,4 +274,4 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default LaporanPublik;
|
||||
export default LaporanPublik;
|
||||
@@ -120,7 +120,7 @@ function EditPencegahanKriminalitas() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Back button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailPencegahanKriminalitas() {
|
||||
const data = kriminalitasState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -54,7 +54,7 @@ function DetailPencegahanKriminalitas() {
|
||||
{/* Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -59,7 +59,7 @@ function CreatePencegahanKriminalitas() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -46,8 +46,9 @@ function PencegahanKriminalitas() {
|
||||
}
|
||||
|
||||
function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
const kriminalitasState = useProxy(pencegahanKriminalitasState)
|
||||
const kriminalitasState = useProxy(pencegahanKriminalitasState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,24 +59,28 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
} = kriminalitasState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Pencegahan Kriminalitas</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title
|
||||
order={4}
|
||||
lh={{ base: 1.2, md: 1.1 }}
|
||||
>
|
||||
Daftar Pencegahan Kriminalitas
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -86,15 +91,22 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Pencegahan</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Deskripsi Singkat</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama Pencegahan</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi Singkat</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -102,35 +114,29 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
data.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
truncate
|
||||
lineClamp={1}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
truncate
|
||||
lineClamp={1}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
truncate
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.deskripsiSingkat || ''
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
truncate
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.deskripsiSingkat || ''
|
||||
}}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -139,7 +145,9 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -148,7 +156,7 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data pencegahan kriminalitas yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -158,6 +166,71 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{data.length > 0 ? (
|
||||
data.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama Pencegahan
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.4}
|
||||
lineClamp={3}
|
||||
ta="justify"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.4}
|
||||
lineClamp={3}
|
||||
ta="justify"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data pencegahan kriminalitas yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -175,8 +248,8 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default PencegahanKriminalitas;
|
||||
export default PencegahanKriminalitas;
|
||||
@@ -247,7 +247,7 @@ function EditPolsekTerdekat() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={modalOpen}
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailPolsekTerdekat() {
|
||||
const data = polsekState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -54,7 +54,7 @@ function DetailPolsekTerdekat() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user