Compare commits

...

4 Commits

Author SHA1 Message Date
503da91ce6 Tambah seeder di bagian landing page 2026-01-06 17:54:21 +08:00
daaed8089b Fix All Search Admin 2026-01-05 17:11:30 +08:00
f436aa2ef0 Fix QC Kak Inno Mobile Done
FIx QC Kak Ayu Mobile Admin Done
Fix Tampilan Admin Mobile Device All Menu Done
2026-01-02 16:33:15 +08:00
50bc54ceca Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Menu Inovasi
2025-12-24 14:36:51 +08:00
198 changed files with 6554 additions and 2585 deletions

View File

@@ -0,0 +1,30 @@
export default async function fetchWithRetry(
url: string,
retries = 3,
timeoutMs = 20000
) {
for (let attempt = 1; attempt <= retries; attempt++) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return res;
} catch (err) {
console.warn(`⚠️ Download attempt ${attempt} failed`);
if (attempt === retries) {
throw err;
}
} finally {
clearTimeout(timeout);
}
}
throw new Error("Unreachable");
}

View File

@@ -1,137 +1,120 @@
[
{
"id": "cmff0rr4z0002vn0twp333m2",
"name": "S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"realName": "bares.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"category": "image"
},
{
"id": "cmff0tnf00003vn0t3kgzi0u0",
"name": "_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"realName": "bicara-darma.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"category": "image"
},
{
"id": "cmff0uykf0004vn0trmmxpgfh",
"name": "bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"realName": "daves.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"category": "image"
},
{
"id": "cmff0z34f0005vn0tjtvq519p",
"name": "Z4hWaV04CvoE20MjccQsV-desktop.webp",
"realName": "mangan.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Z4hWaV04CvoE20MjccQsV-desktop.webp",
"category": "image"
},
{
"id": "cmff38cyq000bvn0t9f01cz3f",
"name": "LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"realName": "gelah-melah.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"category": "image"
},
{
"id": "cmff0zqvd0007vn0tv6o5hjcq",
"name": "gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"realName": "inovasi-desa-darmasaba.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"category": "image"
},
{
"id": "cmff1013m0008vn0th7t0d64d",
"name": "JpL-9F8-IGztMn8E2ce02-desktop.webp",
"realName": "pdkt.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/JpL-9F8-IGztMn8E2ce02-desktop.webp",
"category": "image"
},
{
"id": "cmff10cwq0009vn0tse8dzu3j",
"name": "bxAk4AsGbJTC705_IVdes-desktop.webp",
"realName": "sajjiana-dharma-raksaka.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bxAk4AsGbJTC705_IVdes-desktop.webp",
"category": "image"
},
{
"id": "cmff2w5ly000avn0telhct71k",
"name": "Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"id": "cmk27746i0000vnso2aspwf9g",
"name": "Eqlrr1W-pK8ShMGqgPGL3-desktop.webp",
"realName": "perbekel.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"link": "/api/fileStorage/findUnique/Eqlrr1W-pK8ShMGqgPGL3-desktop.webp",
"category": "image"
}
,
{
"id": "cmk20mg320000vnevxy0k73fr",
"name": "thpgPSJkBxUIRajZt3AVo-desktop.webp",
"realName": "bares.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/thpgPSJkBxUIRajZt3AVo-desktop.webp",
"category": "image"
},
{
"id": "cmff3joae0000vn6h8sgs0ilg",
"name": "7hox9spUxj56hY_EBYLnj-desktop.webp",
"id": "cmk20nqmu0001vnevfte29rk0",
"name": "ubna9N6r7RgVWN5plO5mq-desktop.webp",
"realName": "bicara-darma.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/ubna9N6r7RgVWN5plO5mq-desktop.webp",
"category": "image"
},
{
"id": "cmk228urs0007vnevi5b66bqn",
"name": "Z4i2RRnnlHq2iWj94ldyo-desktop.webp",
"realName": "daves.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Z4i2RRnnlHq2iWj94ldyo-desktop.webp",
"category": "image"
},
{
"id": "cmk20nyen0002vnevd0hfr3u8",
"name": "y4yaE4XdUP1TSUGhWPW9h-desktop.webp",
"realName": "mangan.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/y4yaE4XdUP1TSUGhWPW9h-desktop.webp",
"category": "image"
},
{
"id": "cmk20o7mf0003vnevohrksm1d",
"name": "Vr7CoaYDpk2dIkHx9PxRj-desktop.webp",
"realName": "gelah-melah.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Vr7CoaYDpk2dIkHx9PxRj-desktop.webp",
"category": "image"
},
{
"id": "cmk20of8m0004vnev9ujy5o0l",
"name": "ceoB_sg-HOzljN8j_2nZA-desktop.webp",
"realName": "inovasi-desa-darmasaba.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/ceoB_sg-HOzljN8j_2nZA-desktop.webp",
"category": "image"
},
{
"id": "cmk20omzq0005vnevgi6f4edu",
"name": "vOy5YVUXfHXfiFOHylIN7-desktop.webp",
"realName": "pdkt.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/vOy5YVUXfHXfiFOHylIN7-desktop.webp",
"category": "image"
},
{
"id": "cmk20pf3d0006vnev3mkoqpyy",
"name": "gE_qcqIbY0mqI6FV9V4CL-desktop.webp",
"realName": "sajjiana-dharma-raksaka.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/gE_qcqIbY0mqI6FV9V4CL-desktop.webp",
"category": "image"
},
{
"id": "cmk2cgqgm0003vn96jun52pik",
"name": "q1G995W7cLkC_qquLTlKN-desktop.webp",
"realName": "youtube.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp",
"link": "/api/fileStorage/findUnique/q1G995W7cLkC_qquLTlKN-desktop.webp",
"category": "image"
},
{
"id": "cmff3ll130001vn6hkhls3f5y",
"name": "ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"realName": "gmail.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"category": "image"
},
{
"id": "cmff3mtat0002vn6hs8vyyhdd",
"name": "z8v9ZREwOJHKGIRYauROt-desktop.webp",
"id": "cmk2cmr000006vn96qepq6gvl",
"name": "I6mlQ4nRmPX26gm79C_rM-desktop.webp",
"realName": "facebook.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp",
"link": "/api/fileStorage/findUnique/I6mlQ4nRmPX26gm79C_rM-desktop.webp",
"category": "image"
},
{
"id": "cmff3nv180003vn6h5jvedidq",
"name": "BLjMxTKoCNE31uOURR3IU-desktop.webp",
"realName": "telephone-call.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/BLjMxTKoCNE31uOURR3IU-desktop.webp",
"category": "image"
},
{
"id": "cmff3oouh0004vn6hd94brzv9",
"name": "hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"id": "cmk2cpeba0009vn966jcrpf3u",
"name": "WArLC_yvU33MjoqEnQeQ1-desktop.webp",
"realName": "instagram.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"link": "/api/fileStorage/findUnique/WArLC_yvU33MjoqEnQeQ1-desktop.webp",
"category": "image"
},
{
"id": "cmff3q12g0005vn6h5ojov2qa",
"name": "6XEoZ9SFu59COpil03Gya-desktop.webp",
"id": "cmk2crcl1000cvn96j8pmgmo5",
"name": "D3RPbNiaNSCjacLjeR_qO-desktop.webp",
"realName": "tiktok.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp",
"link": "/api/fileStorage/findUnique/D3RPbNiaNSCjacLjeR_qO-desktop.webp",
"category": "image"
}
]

View File

@@ -3,24 +3,24 @@
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
"imageId": "cmff3joae0000vn6h8sgs0ilg"
"imageId": "cmk2cgqgm0003vn96jun52pik"
},
{
"id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku",
"imageId": "cmff3mtat0002vn6hs8vyyhdd"
"imageId": "cmk2cmr000006vn96qepq6gvl"
},
{
"id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/",
"imageId": "cmff3oouh0004vn6hd94brzv9"
"imageId": "cmk2cpeba0009vn966jcrpf3u"
},
{
"id": "cmds92de5000hvnbemlu6sq5x",
"name": "desa.darmasaba",
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
"imageId": "cmff3q12g0005vn6h5ojov2qa"
"imageId": "cmk2crcl1000cvn96j8pmgmo5"
}
]

View File

@@ -3,6 +3,6 @@
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027",
"imageId": "cmff2w5ly000avn0telhct71k"
"imageId": "cmk2a2dl6001nvngck1n0k8qc"
}
]

View File

@@ -4,48 +4,55 @@
"name": "Dmangan",
"description": "Darmasaba Aman Pangan",
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
"imageId" : "cmff0z34f0005vn0tjtvq519p"
"imageId" : "cmk20nyen0002vnevd0hfr3u8"
},
{
"id": "cmdr76nqk0008vn5rdddvcxnr",
"name": "Bicara Darmasaba",
"description": "Bicara Darmasaba",
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
"imageId" : "cmff0tnf00003vn0t3kgzi0u0"
"imageId" : "cmk20nqmu0001vnevfte29rk0"
},
{
"id": "cmdr77vbw000bvn5rvpmoq31s",
"name": "Bares",
"description": "Darmasaba Recycling Stock/Exchange",
"link": "http://darmasaba.desa.id/berita/56722-bares",
"imageId" : "cmff0rr4z0002vn0twp333m2"
"imageId" : "cmk20mg320000vnevxy0k73fr"
},
{
"id": "cmdr7bxtp000evn5rmy85wihx",
"name": "Sajjana Dharma Raksaka",
"description": "Sajjana Dharma Raksaka",
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
"imageId" : "cmff10cwq0009vn0tse8dzu3j"
"imageId" : "cmk20pf3d0006vnev3mkoqpyy"
},
{
"id": "cmdr7dlnk000hvn5r9lur3z35",
"name": "PDKT",
"description": "Perangkat Desa Kuat Teknologi",
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
"imageId" : "cmff1013m0008vn0th7t0d64d"
"imageId" : "cmk20omzq0005vnevgi6f4edu"
},
{
"id": "cmdr7ftob000mvn5rfhgdtg8v",
"name": "GM",
"description": "Galah Melah",
"link": "https://darmasaba.desa.id/berita/52880-galah-melah",
"imageId" : "cmff38cyq000bvn0t9f01cz3f"
"imageId" : "cmk20o7mf0003vnevohrksm1d"
},
{
"id": "cmdr7glue000pvn5r6onzslju",
"name": "Inovasi Desa Darmasaba",
"description": "Inovasi Desa Darmasaba",
"link": "https://darmasaba.desa.id/produk-lokal-desa",
"imageId" : "cmff0zqvd0007vn0tv6o5hjcq"
"imageId" : "cmk20of8m0004vnev9ujy5o0l"
},
{
"id": "cmk228ust0009vnev5p8i377o",
"name": "Davest",
"description": "<p>DAVEST (Darmasaba Investment) merupakan program inovasi Desa Darmasaba yang bertujuan mempromosikan potensi investasi desa secara terintegrasi melalui media digital dan pendampingan langsung. Program ini menjadi sarana penghubung antara pemerintah desa, pelaku usaha, dan investor dalam rangka mendorong pertumbuhan ekonomi desa yang berkelanjutan.</p><p>DAVEST menyajikan informasi potensi unggulan desa seperti sektor UMKM, pariwisata, ekonomi kreatif, serta peluang investasi berbasis sumber daya lokal dengan prinsip transparansi dan kemudahan akses informasi.</p><p>Di tahun 2024 ini Davest (Darmasaba Village Festival) akan diadakan lagi, dengan berbagai kegiatan pemerdayaan, edukasi dan hiburan yang tentunya lebih waahhhh dari dua tahun lalu. Untuk memantapkan hal tersebut, Pemdes Darmasaba melakukan rapat koordinasi (rakor) Davest 2024 yang dipimpin langsung oleh Perbekel Darmasaba I. B. Surya Prabhawa Manuaba, S.H.,M.H. pada hari Senin (22/1/2024) bertempat di Ruang Shanti Gosana Kantor Perbekel Darmasaba.</p><hr><h3>Tujuan Program</h3><ul><li><p>Meningkatkan daya tarik investasi di Desa Darmasaba</p></li><li><p>Mempromosikan potensi unggulan desa secara profesional</p></li><li><p>Mendorong pertumbuhan ekonomi dan penciptaan lapangan kerja</p></li><li><p>Mendukung visi Desa Darmasaba sebagai desa inovatif dan berdaya saing</p></li></ul><hr><h3>Sasaran Program</h3><ul><li><p>Calon investor lokal dan regional</p></li><li><p>Pelaku UMKM dan kelompok usaha desa</p></li><li><p>Masyarakat Desa Darmasaba</p></li></ul><hr><h3>Bentuk Inovasi</h3><ul><li><p>Inovasi ekonomi desa</p></li><li><p>Inovasi digital</p></li><li><p>Inovasi tata kelola pelayanan investasi</p></li></ul><hr><h3>Ruang Lingkup Kegiatan</h3><ul><li><p>Penyusunan profil potensi investasi desa</p></li><li><p>Digitalisasi informasi investasi desa</p></li><li><p>Promosi peluang investasi melalui media online</p></li><li><p>Fasilitasi komunikasi antara investor dan desa</p></li><li><p>Pendampingan awal investasi berbasis desa</p></li></ul>",
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024",
"imageId" : "cmk228urs0007vnevi5b66bqn"
}
]

View File

@@ -0,0 +1,11 @@
import safeImageId from "./safeImageId";
export default async function resolveImageIdForSeed(
existingImageId: string | null | undefined,
seedImageId: string | null | undefined
) {
if (existingImageId) return existingImageId;
// ✅ Skip validasi saat seed
return await safeImageId(seedImageId, true);
}

View File

@@ -0,0 +1,24 @@
import prisma from "@/lib/prisma";
export default async function safeImageId(
imageId?: string | null,
skipValidation = false // ✅ tambah param
) {
if (!imageId) return null;
if (skipValidation) {
console.log(`⚠️ Skipping validation for ${imageId} (seed mode)`);
return imageId; // langsung return tanpa cek DB
}
const exists = await prisma.fileStorage.findUnique({
where: { id: imageId },
});
if (!exists) {
console.warn(`⚠️ imageId ${imageId} not found in FileStorage`);
return null;
}
return imageId;
}

View File

@@ -0,0 +1,142 @@
/*
Warnings:
- You are about to drop the column `dokterdanTenagaMedisId` on the `FasilitasKesehatan` table. All the data in the column will be lost.
- You are about to drop the column `tarifDanLayananId` on the `FasilitasKesehatan` table. All the data in the column will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `UserSession` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `permissions` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_dokterdanTenagaMedisId_fkey";
-- DropForeignKey
ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_tarifDanLayananId_fkey";
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_roleId_fkey";
-- DropForeignKey
ALTER TABLE "UserSession" DROP CONSTRAINT "UserSession_userId_fkey";
-- AlterTable
ALTER TABLE "DokterdanTenagaMedis" ADD COLUMN "jadwalLibur" TEXT,
ADD COLUMN "jamBukaLibur" TEXT,
ADD COLUMN "jamBukaOperasional" TEXT,
ADD COLUMN "jamTutupLibur" TEXT,
ADD COLUMN "jamTutupOperasional" TEXT;
-- AlterTable
ALTER TABLE "FasilitasKesehatan" DROP COLUMN "dokterdanTenagaMedisId",
DROP COLUMN "tarifDanLayananId";
-- AlterTable
ALTER TABLE "MediaSosial" ADD COLUMN "icon" TEXT;
-- AlterTable
ALTER TABLE "roles" ALTER COLUMN "permissions" DROP NOT NULL;
-- DropTable
DROP TABLE "User";
-- DropTable
DROP TABLE "UserSession";
-- DropTable
DROP TABLE "permissions";
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"nomor" TEXT NOT NULL,
"roleId" TEXT NOT NULL DEFAULT '2',
"isActive" BOOLEAN NOT NULL DEFAULT false,
"sessionInvalid" BOOLEAN NOT NULL DEFAULT false,
"lastLogin" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"permissions" JSONB,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_sessions" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserMenuAccess" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuAccess_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_Tarif" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_Tarif_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_Dokter" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_Dokter_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_nomor_key" ON "users"("nomor");
-- CreateIndex
CREATE INDEX "user_sessions_userId_idx" ON "user_sessions"("userId");
-- CreateIndex
CREATE INDEX "user_sessions_token_idx" ON "user_sessions"("token");
-- CreateIndex
CREATE UNIQUE INDEX "UserMenuAccess_userId_menuId_key" ON "UserMenuAccess"("userId", "menuId");
-- CreateIndex
CREATE INDEX "_Tarif_B_index" ON "_Tarif"("B");
-- CreateIndex
CREATE INDEX "_Dokter_B_index" ON "_Dokter"("B");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuAccess" ADD CONSTRAINT "UserMenuAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_B_fkey" FOREIGN KEY ("B") REFERENCES "TarifDanLayanan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_A_fkey" FOREIGN KEY ("A") REFERENCES "DokterdanTenagaMedis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_B_fkey" FOREIGN KEY ("B") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,30 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// helpers/safeSeedUnique.ts
import prisma from "@/lib/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
type SafeSeedOptions = {
skipUpdate?: boolean;
};
/**
* Helper generic buat seed dengan upsert aman
*/
// prisma/safeseedUnique.ts
export async function safeSeedUnique<T extends keyof PrismaClient>(
model: T,
where: Record<string, any>,
data: Record<string, any>
data: Record<string, any>,
options: SafeSeedOptions = {}
) {
const m = prisma[model];
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
const m = prisma[model] as any;
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`);
try {
// @ts-expect-error upsert dynamic
await m.upsert({
// Pastikan `where` berisi field yang benar-benar unique (misal: `id`)
const result = await m.upsert({
where,
update: data,
create: { ...where, ...data },
update: options.skipUpdate ? {} : data,
create: data, // ✅ Jangan duplikasi `where` ke `create`
});
console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`);
console.log(`✅ Seed ${String(model)}:`, where);
return result;
} catch (err) {
console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err);
console.error(`❌ Gagal seed ${String(model)}:`, where, err);
throw err; // ✅ Rethrow agar seeding berhenti jika kritis
}
}
// /* eslint-disable @typescript-eslint/no-explicit-any */
// import { PrismaClient } from "@prisma/client";
// const prisma = new PrismaClient();
// type SafeSeedOptions = {
// skipUpdate?: boolean;
// };
// export async function safeSeedUnique<T extends keyof PrismaClient>(
// model: T,
// where: Record<string, any>,
// data: Record<string, any>,
// options: SafeSeedOptions = {}
// ) {
// const m = prisma[model] as any;
// if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`);
// try {
// await m.upsert({
// where,
// update: options.skipUpdate ? {} : data,
// create: { ...where, ...data },
// });
// console.log(`✅ Seed ${String(model)}:`, where);
// } catch (err) {
// console.error(`❌ Gagal seed ${String(model)}:`, where, err);
// }
// }

View File

@@ -60,8 +60,37 @@ import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan
import seedAssets from "./seed_assets";
import users from "./data/user/users.json";
import { safeSeedUnique } from "./safeseedUnique";
import safeImageId from "./data/safeImageId";
import resolveImageIdForSeed from "./data/resolveImageId";
(async () => {
// seed assets
await prisma.fileStorage.deleteMany();
console.log("🗑️ Cleared existing fileStorage records");
await seedAssets();
// // =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
await safeSeedUnique(
"fileStorage",
{ name: f.name },
{
id: f.id,
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
deletedAt: null,
isActive: true,
}
);
}
console.log("✅ File storage seeded");
console.log("🔄 Seeding roles...");
for (const r of roles) {
@@ -131,112 +160,119 @@ import { safeSeedUnique } from "./safeseedUnique";
}
}
console.log("✅ Users seeding completed");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
try {
await prisma.fileStorage.upsert({
where: { id: f.id },
update: {
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
create: {
id: f.id,
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
});
} catch (error: any) {
console.error(`❌ Failed to seed file storage ${f.name}:`, error.message);
}
}
console.log("✅ File storage seeded");
// =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA ===========
// In your seed.ts file, update the PejabatDesa seeding section to:
console.log("🔄 Seeding Pejabat Desa...");
for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
position: p.position,
imageId: p.imageId,
},
create: {
id: p.id,
name: p.name,
position: p.position,
imageId: p.imageId,
},
});
try {
// First, verify the image exists
if (p.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
where: { id: p.imageId },
});
if (!imageExists) {
console.warn(
`⚠️ Image not found for PejabatDesa ${p.name}, skipping...`
);
continue;
}
}
await safeSeedUnique(
"pejabatDesa",
{ id: p.id },
{
id: p.id,
name: p.name,
position: p.position,
imageId: p.imageId,
}
);
console.log(`✅ Seeded Pejabat Desa -> ${p.name}`);
} catch (error: any) {
console.error(`❌ Failed to seed Pejabat Desa ${p.name}:`, error.message);
}
}
console.log(
"✅ profilePejabatDesa seeded without imageId (editable later via UI)"
);
console.log("✅ Pejabat Desa seeding completed");
// =========== PROGRAM INOVASI ===========
for (const p of programInovasi) {
let imageId: string | null = null;
// Add this section after the other seed operations in seed.ts
console.log("🔄 Seeding Program Inovasi...");
if (p.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
for (const p of programInovasi) {
const existing = await prisma.programInovasi.findUnique({
where: { id: p.id },
select: { imageId: true },
});
let imageId = existing?.imageId; // Pertahankan existing
// Kalau belum ada imageId, cari berdasarkan name/realName
if (!imageId && p.imageId) {
// ✅ Cari langsung berdasarkan ID yang ada di p.imageId
const fileRecord = await prisma.fileStorage.findUnique({
where: { id: p.imageId },
select: { id: true, name: true },
});
if (imageExists) {
imageId = p.imageId;
} else {
console.warn(
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
if (fileRecord) {
imageId = fileRecord.id;
console.log(
`✅ Found file by ID: ${fileRecord.name} (${fileRecord.id})`
);
} else {
console.warn(`⚠️ File with ID ${p.imageId} not found for ${p.name}`);
imageId = null;
}
}
await prisma.programInovasi.upsert({
where: { id: p.id },
update: {
name: p.name,
description: p.description,
link: p.link,
imageId: p.imageId,
imageId,
},
create: {
id: p.id,
name: p.name,
description: p.description,
link: p.link,
imageId: p.imageId,
imageId,
},
});
}
console.log("program inovasi success ...");
// =========== MEDIA SOSIAL ===========
for (const p of mediaSosial) {
for (const m of mediaSosial) {
const existing = await prisma.mediaSosial.findUnique({
where: { id: m.id },
select: { imageId: true },
});
const imageId = await resolveImageIdForSeed(existing?.imageId, m.imageId);
await prisma.mediaSosial.upsert({
where: { id: p.id },
where: { id: m.id },
update: {
name: p.name,
iconUrl: p.iconUrl,
imageId: p.imageId,
name: m.name,
iconUrl: m.iconUrl,
// ⛔ JANGAN overwrite imageId sembarangan
imageId,
},
create: {
id: p.id,
name: p.name,
iconUrl: p.iconUrl,
imageId: p.imageId,
id: m.id,
name: m.name,
iconUrl: m.iconUrl,
imageId,
},
});
}
console.log("media sosial success ...");
// =========== SUBMENU DESA ANTI KORUPSI ===========
@@ -1245,9 +1281,6 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("✅ Jenjang Pendidikan seeded successfully");
// seed assets
await seedAssets();
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// prisma/seedAssets.ts
import prisma from "@/lib/prisma";
import AdmZip from "adm-zip";
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
import fetch from "node-fetch";
import AdmZip from "adm-zip";
import prisma from "@/lib/prisma";
import fetchWithRetry from "./data/fetchWithRetry";
const UPLOADS_DIR =
process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads");
@@ -18,7 +19,10 @@ function detectCategory(filename: string): "image" | "document" | "other" {
}
// --- Helper: recursive walk dir ---
async function walkDir(dir: string, fileList: string[] = []): Promise<string[]> {
async function walkDir(
dir: string,
fileList: string[] = []
): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
@@ -41,18 +45,45 @@ export default async function seedAssets() {
// 1. Download zip
const url =
"https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1";
const res = await fetch(url);
if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
"https://cld-dkr-makuro-seafile.wibudev.com/f/90dd12c9713e42379fcd/?dl=1";
const res = await fetchWithRetry(url, 3, 20000);
// Validasi content-type
const contentType = res.headers.get("content-type");
if (!contentType?.includes("zip")) {
throw new Error(`Invalid content-type (${contentType}). Expected ZIP file`);
}
const buffer = Buffer.from(await res.arrayBuffer());
// Validasi ukuran file
if (buffer.length < 100) {
throw new Error("Downloaded ZIP is empty or corrupted");
}
// Validasi signature ZIP ("PK")
if (buffer.toString("utf8", 0, 2) !== "PK") {
throw new Error("Invalid ZIP signature (PK not found)");
}
// 2. Extract zip ke folder tmp
const extractDir = path.join(process.cwd(), "tmp_assets");
await fs.rm(extractDir, { recursive: true, force: true });
await fs.mkdir(extractDir, { recursive: true });
const zip = new AdmZip(buffer);
zip.extractAllTo(extractDir, true);
let zip: AdmZip;
try {
zip = new AdmZip(buffer);
} catch (err) {
throw new Error("Failed to parse ZIP file (corrupted or invalid)");
}
try {
zip.extractAllTo(extractDir, true);
} catch (err) {
throw new Error("Failed to extract ZIP contents");
}
// 3. Cari semua file valid (recursive)
const files = await walkDir(extractDir);
@@ -84,18 +115,41 @@ export default async function seedAssets() {
await fs.copyFile(filePath, targetPath);
}
// 5. Simpan ke DB
await prisma.fileStorage.create({
data: {
name: finalName,
realName: entryName,
path: targetPath,
mimeType,
link: `/uploads/${category}/${finalName}`,
category,
},
const existing = await prisma.fileStorage.findUnique({
where: { name: finalName },
});
if (existing) {
// Restore kalau soft deleted
await prisma.fileStorage.update({
where: { name: finalName },
data: {
path: targetPath,
realName: entryName,
mimeType,
link: `/uploads/${category}/${finalName}`,
category,
deletedAt: null,
isActive: true,
},
});
console.log(`♻️ restored: ${category}/${finalName}`);
} else {
await prisma.fileStorage.create({
data: {
name: finalName,
realName: entryName,
path: targetPath,
mimeType,
link: `/uploads/${category}/${finalName}`,
category,
},
});
console.log(`📂 created: ${category}/${finalName}`);
}
console.log(`📂 saved: ${category}/${finalName}`);
}
@@ -103,6 +157,8 @@ export default async function seedAssets() {
await fs.rm(extractDir, { recursive: true, force: true });
console.log("✅ Selesai seed assets!");
console.log("DB URL (asset):", process.env.DATABASE_URL);
}
// --- Auto run kalau dipanggil langsung ---

View File

@@ -68,7 +68,7 @@ const category = proxy({
const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany"
].get({
query: { page, limit },
query: { page, limit, search },
});
if (res.status === 200 && res.data?.success) {

View File

@@ -65,7 +65,7 @@ const potensiDesa = proxy({
const res = await ApiFetch.api.desa.potensi[
"find-many"
].get({
query: { page, limit },
query: { page, limit, search },
});
if (res.status === 200 && res.data?.success) {

View File

@@ -312,15 +312,15 @@ const kategoriProduk = proxy({
page: 1,
totalPages: 1,
loading: false,
search2: "",
load: async (page = 1, limit = 10, search2 = "") => {
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriProduk.findMany.page = page;
kategoriProduk.findMany.search2 = search2;
kategoriProduk.findMany.search = search;
try {
const query: any = { page, limit };
if (search2) query.search2 = search2;
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });

View File

@@ -194,7 +194,7 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();

View File

@@ -60,13 +60,18 @@ const responden = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
responden.findMany.loading = true; // Use the full path to access the property
responden.findMany.page = page;
responden.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.responden["findMany"].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {

View File

@@ -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";
@@ -65,13 +66,46 @@ const dataPendidikan = proxy({
select: { id: true; name: true; jumlah: true };
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.datapendidikan[
"findMany"
].get();
if (res.status === 200) {
dataPendidikan.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
dataPendidikan.findMany.loading = true; // Use the full path to access the property
dataPendidikan.findMany.page = page;
dataPendidikan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.datapendidikan[
"findMany"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
dataPendidikan.findMany.data = res.data.data || [];
dataPendidikan.findMany.total = res.data.total || 0;
dataPendidikan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load data pendidikan:",
res.data?.message
);
dataPendidikan.findMany.data = [];
dataPendidikan.findMany.total = 0;
dataPendidikan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading data pendidikan:", error);
dataPendidikan.findMany.data = [];
dataPendidikan.findMany.total = 0;
dataPendidikan.findMany.totalPages = 1;
} finally {
dataPendidikan.findMany.loading = false;
}
},
},

View File

@@ -220,11 +220,34 @@ const roleState = proxy({
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.role["findMany"].get();
if (res.status === 200) {
roleState.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
roleState.findMany.loading = true; // ✅ Akses langsung via nama path
roleState.findMany.page = page;
roleState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.role["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
roleState.findMany.data = res.data.data ?? [];
roleState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
roleState.findMany.data = [];
roleState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch role paginated:", err);
roleState.findMany.data = [];
roleState.findMany.totalPages = 1;
} finally {
roleState.findMany.loading = false;
}
},
},

View File

@@ -73,17 +73,17 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
>
{/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<ScrollArea type="auto" offsetScrollbars w="100%">
<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
width: "max-content", // ⬅️ kunci
maxWidth: "100%",
}}
>
{tabs.map((tab, i) => (

View File

@@ -88,63 +88,65 @@ function ListVideo({ search }: { search: string }) {
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover w="100%">
<TableThead>
<TableTr>
<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>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lh={1.45}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
px="xs"
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<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={{ maxWidth: 250 }}>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" c="dimmed" lh={1.45}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
px="xs"
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
)}
</TableTbody>
</Table>
</Box>
</Box>
{/* Mobile Cards */}

View File

@@ -5,8 +5,7 @@ import {
Button,
Center,
Divider,
Grid,
GridCol,
Group,
Paper,
Skeleton,
Stack,
@@ -43,32 +42,29 @@ function PelayananPendudukNonPermanent() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<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>
</GridCol>
</Grid>
<Group justify='space-between' align="center">
<Title
order={3}
lh={1.2}
c={colors['blue-button']}
>
Preview Pelayanan Penduduk Non Permanen
</Title>
<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>
</Group>
{/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">

View File

@@ -6,8 +6,6 @@ import {
Button,
Center,
Divider,
Grid,
GridCol,
Group,
Paper,
Skeleton,
@@ -76,28 +74,24 @@ function PerizinanBerusaha() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<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>
</GridCol>
</Grid>
<Group justify='space-between' align="center">
<Title order={3} c={colors['blue-button']} lh={1.2}>
Preview Pelayanan Perizinan Berusaha
</Title>
<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>
</Group>
{/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
@@ -136,7 +130,7 @@ function PerizinanBerusaha() {
umum:
</Text>
<Box p="xl" w="100%" visibleFrom='md'>
<Box p="xl" w="100%" visibleFrom='md'>
<Stepper
active={active}
onStepClick={setActive}
@@ -221,37 +215,37 @@ function PerizinanBerusaha() {
>
<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>

View File

@@ -1,7 +1,7 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -31,22 +31,18 @@ function Page() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header + tombol edit */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<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/profil/profil-perbekel/${perbekel.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Group justify="space-between">
<Title order={2} c={colors['blue-button']} lh={1.2}>Profil Perbekel</Title>
<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>
</Group>
{/* Card Profil */}
<Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs">
@@ -60,7 +56,7 @@ function Page() {
<GridCol span={12}>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
fz={{ base: 'sm', md: 'xl' }}
fw="bold"
c={colors['blue-button']}
lh={{ base: 1.45, md: 1.45 }}

View File

@@ -166,7 +166,7 @@ function ListAPBDesa({ search }: { search: string }) {
<TableTd>
<Button
variant="light"
color="green"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
@@ -243,7 +243,7 @@ function ListAPBDesa({ search }: { search: string }) {
</Box>
<Button
variant="light"
color="green"
color="blue"
fullWidth
onClick={() =>
router.push(

View File

@@ -128,10 +128,18 @@ function ListBelanja({ search }: { search: string }) {
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>

View File

@@ -120,12 +120,20 @@ function ListPembiayaan({ search }: { search: string }) {
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
<TableThead>
<TableTr>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>

View File

@@ -160,7 +160,6 @@ function ListPendapatan({ search }: { search: string }) {
px="xs"
>
<IconEdit size={16} />
<Text ml={5}>Edit</Text>
</Button>
</TableTd>
<TableTd>
@@ -176,7 +175,6 @@ function ListPendapatan({ search }: { search: string }) {
px="xs"
>
<IconTrash size={16} />
<Text ml={5}>Hapus</Text>
</Button>
</TableTd>
</TableTr>

View File

@@ -153,9 +153,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
</Text>
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.45} c="dimmed" lineClamp={2}>
{item.deskripsi || '-'}
</Text>
<Text
fz="sm"
fw={500}
lh={1.45}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.45}>{item.hierarki || '-'}</Text>
@@ -223,9 +228,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.deskripsi || '-'}
</Text>
<Text
fz="sm"
fw={500}
lh={1.45}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>

View File

@@ -21,7 +21,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -59,6 +59,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
@@ -79,8 +81,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
useEffect(() => {
if (data) {

View File

@@ -28,27 +28,27 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function KategoriProduk() {
const [search2, setSearch2] = useState('');
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kategori Produk'
placeholder='Cari nama kategori produk...'
searchIcon={<IconSearch size={20} />}
value={search2}
onChange={(e) => setSearch2(e.currentTarget.value)}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriProduk search2={search2} />
<ListKategoriProduk search={search} />
</Box>
);
}
function ListKategoriProduk({ search2 }: { search2: string }) {
function ListKategoriProduk({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search2, 1000);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePasar.findMany;

View File

@@ -194,7 +194,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={4}>
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Judul Program

View File

@@ -40,7 +40,7 @@ function DetailAjukanIdeInofativDesa() {
const data = state.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -54,7 +54,7 @@ function DetailAjukanIdeInofativDesa() {
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "80%", lg: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -17,7 +17,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -44,6 +44,7 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
@@ -54,8 +55,8 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
} = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []

View File

@@ -111,7 +111,7 @@ function EditDigitalSmartVillage() {
};
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">

View File

@@ -49,7 +49,7 @@ function DetailDesaDigital() {
const data = stateDesaDigital.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -63,7 +63,7 @@ function DetailDesaDigital() {
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -77,12 +77,12 @@ function DetailDesaDigital() {
{/* Sub Card Detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Box pl={5}>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Box pl={5}>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"

View File

@@ -73,7 +73,7 @@ export default function CreateDesaDigital() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan tombol kembali */}
<Group mb="md" align="center">
<Button

View File

@@ -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,28 +45,31 @@ function DesaDigitalSmartVillage() {
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.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="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Desa Digital Smart Village</Title>
<Title order={4} lh={1.2}>
List Desa Digital Smart Village
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -78,15 +81,34 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="sm">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Inovasi</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat Inovasi
<TableTh style={{ width: '30%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Nama Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '50%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -94,21 +116,18 @@ function ListDesaDigitalSmartVillage({ 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">
{item.name}
</Text>
</TableTd>
<TableTd>
<Box w={200}>
<Text
fz="sm"
c="dimmed"
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
<Text
fz="sm"
c="dimmed"
lh={1.5}
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd>
<Button
@@ -121,7 +140,9 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</TableTd>
</TableTr>
@@ -129,8 +150,8 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
@@ -140,6 +161,64 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="sm">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Inovasi
</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
<Box pl={5}>
<Text
fz="sm"
fw={500}
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() =>
router.push(
`/admin/inovasi/desa-digital-smart-village/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination

View File

@@ -123,7 +123,7 @@ function EditInfoTeknologiTepatGuna() {
};
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">

View File

@@ -40,7 +40,7 @@ function DetailInfoTeknologiTepatGuna() {
const data = stateInfoTekno.findUnique.data
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -54,7 +54,7 @@ function DetailInfoTeknologiTepatGuna() {
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -74,7 +74,7 @@ function CreateInfoTeknologiTepatGuna() {
};
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">

View File

@@ -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 { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -45,28 +45,31 @@ function InfoTeknologiTepatGuna() {
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.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: '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: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Info Teknologi Tepat Guna</Title>
<Title order={4} lh={1.2}>
Daftar Info Teknologi Tepat Guna
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -79,40 +82,57 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
</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: '30%' }}>
Nama Info Teknologi
<Text fz="sm" fw={600} lh={1.4}>
Nama Info Teknologi
</Text>
</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '50%' }}>
<TableTd>
<Text
fz="sm"
lh={1.5}
c="dimmed"
lineClamp={1}
truncate
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</TableTd>
<TableTd style={{ width: '20%' }}>
<TableTd>
<Button
size="xs"
radius="md"
@@ -134,7 +154,7 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Text fz="sm" c="dimmed" ta="center">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
@@ -144,6 +164,63 @@ function ListInfoTeknologiTepatGuna({ 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}>
Nama Info Teknologi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz={"sm"} fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Box pl={5}>
<Text
fz="sm"
fw={500}
lh={1.4}
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</Box>
</Box>
<Group justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text fz="sm" c="dimmed" ta="center">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
@@ -164,4 +241,4 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
);
}
export default InfoTeknologiTepatGuna;
export default InfoTeknologiTepatGuna;

View File

@@ -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 { IconListDetails, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -57,36 +57,76 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper biar rapi */}
<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",
}}
>
{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>
<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}

View File

@@ -1,7 +1,29 @@
'use client'
import React from 'react';
import LayoutTabsKolaborasi 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 (
<LayoutTabsKolaborasi>
{children}

View File

@@ -118,7 +118,7 @@ function EditKolaborasiInovasi() {
};
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} />

View File

@@ -41,7 +41,7 @@ function DetailKolaborasiInovasi() {
const data = kolaborasiState.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */}
<Button
variant="subtle"

View File

@@ -55,7 +55,7 @@ function CreateProgramKreatifDesa() {
};
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">

View File

@@ -19,6 +19,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -45,69 +46,105 @@ function KolaborasiInovasi() {
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, loading, page, totalPages, load } = listState.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: 'xs', 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 Kolaborasi Inovasi</Title>
<Box py={{ base: 'xs', 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}>
Daftar Kolaborasi Inovasi
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
onClick={() =>
router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')
}
>
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: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%' }}>Tahun</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
<TableTh style={{ width: '5%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
No
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="xs" fw={600} lh={1.4}>
Nama Kolaborasi Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
Tahun
</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>
<Text fz="xs" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
<TableTd ta="center">
<Text fz="sm" fw={500} lh={1.45}>
{index + 1}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
<Text fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd ta="center">
<Text fz="sm" fw={500} lh={1.45}>
{item.tahun}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={1} c="dimmed">
{item.slug}
</Text>
<Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
</TableTd>
<TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
@@ -115,7 +152,9 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
router.push(
`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`
)
}
>
Detail
@@ -127,7 +166,9 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kolaborasi inovasi yang tersedia</Text>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kolaborasi inovasi yang tersedia
</Text>
</Center>
</TableTd>
</TableTr>
@@ -135,23 +176,93 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<Paper key={item.id} withBorder radius="md" p="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Kolaborasi Inovasi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
</Box>
<Box>
<Button
fullWidth
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`
)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kolaborasi inovasi yang tersedia
</Text>
</Center>
)}
</Stack>
</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>
{totalPages > 1 && (
<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 KolaborasiInovasi;
export default KolaborasiInovasi;

View File

@@ -136,7 +136,7 @@ function EditMitraKolaborasi() {
};
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">

View File

@@ -68,7 +68,7 @@ function CreateMitraKolaborasi() {
};
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">

View File

@@ -18,7 +18,7 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -27,6 +27,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { useDebouncedValue } from '@mantine/hooks';
function MitraKolaborasi() {
const [search, setSearch] = useState('');
@@ -59,27 +60,34 @@ function ListMitraKolaborasi({ search }: { search: string }) {
}
};
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, loading, page, totalPages, load } = listState.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="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Mitra Kolaborasi</Title>
<Group justify="space-between" mb="lg">
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Daftar Mitra Kolaborasi
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -93,41 +101,82 @@ function ListMitraKolaborasi({ search }: { search: string }) {
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: '10%' }}>No</TableTh>
<TableTh style={{ width: '30%' }}>Nama Mitra</TableTh>
<TableTh style={{ width: '25%' }}>Image</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
No
</Text>
</TableTh>
<TableTh style={{ width: '30%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Nama Mitra
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Image
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Delete
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Edit
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
<Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh={1.5}>
{index + 1}
</Text>
</TableTd>
<TableTd>
<Text
fz={{ base: 'sm', md: 'md' }}
fw={500}
lh={1.5}
truncate="end"
lineClamp={1}
>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box
w={70}
h={70}
>
<Box w={70} h={70}>
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
radius="sm"
/>
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
<Box
bg={colors['blue-button']}
w="100%"
h="100%"
/>
)}
</Box>
</TableTd>
@@ -167,8 +216,8 @@ function ListMitraKolaborasi({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
@@ -178,7 +227,96 @@ function ListMitraKolaborasi({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Mitra
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Image
</Text>
<Box w="100%" h={60} mt="xs">
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
radius="sm"
h="100%"
/>
) : (
<Box
bg={colors['blue-button']}
w="100%"
h="100%"
/>
)}
</Box>
</Box>
<Group justify="flex-end" mt="md" gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="red"
disabled={mitraKolaborasi.delete.loading || !item}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={16} />
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="green"
disabled={!item}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -187,14 +325,13 @@ function ListMitraKolaborasi({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
mt="lg"
mb="lg"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
@@ -205,4 +342,4 @@ function ListMitraKolaborasi({ search }: { search: string }) {
);
}
export default MitraKolaborasi;
export default MitraKolaborasi;

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
Box,
ScrollArea,
Stack,
Tabs,
@@ -84,36 +85,76 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
keepMounted={false}
>
{/* ✅ Scroll horizontal biar gak overflow */}
<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",
}}
>
{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>
<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}

View File

@@ -42,8 +42,8 @@ function DetailAdministrasiOnline() {
const data = stateAdminOnline.findUnique.data;
return (
<Box py={10}>
<Group justify='space-between' align='center' w={{ base: "100%", md: "50%" }} mb={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group justify='space-between' align='center' w={{ base: "100%", md: "70%" }} mb={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"

View File

@@ -17,7 +17,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -44,36 +44,55 @@ function AdministrasiOnline() {
function ListAdministrasiOnline({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.administrasiOnline);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.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">
<Title order={4}>Daftar Administrasi Online</Title>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4} lh={1.2} mb={{ base: 'md', md: 'lg' }}>
Daftar Administrasi Online
</Title>
<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%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Alamat</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Aksi</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -81,17 +100,17 @@ function ListAdministrasiOnline({ 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.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
<Text fz="sm" c="gray.7" lh={1.5} lineClamp={1}>
{item.alamat}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
<Text fz="sm" c="gray.7" lh={1.5} lineClamp={1}>
{item.nomorTelepon || '-'}
</Text>
</TableTd>
@@ -116,8 +135,10 @@ function ListAdministrasiOnline({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data administrasi online yang cocok</Text>
<Center py={24}>
<Text c="gray.6" fz="sm" lh={1.4}>
Tidak ada data administrasi online yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -125,6 +146,55 @@ function ListAdministrasiOnline({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="md" withBorder>
<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}>Alamat</Text>
<Text fz="sm" fw={500} c="gray.7" lh={1.5}>{item.alamat}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
<Text fz="sm" fw={500} c="gray.7" lh={1.5}>{item.nomorTelepon || '-'}</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/administrasi-online/${item.id}`
)
}
fullWidth
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="gray.6" fz="sm" lh={1.4}>
Tidak ada data administrasi online yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>

View File

@@ -90,7 +90,7 @@ function EditJenisLayanan() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button

View File

@@ -40,7 +40,7 @@ function DetailJenisLayanan() {
const data = state.findUnique.data
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */}
<Button
variant="subtle"
@@ -54,7 +54,7 @@ function DetailJenisLayanan() {
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -50,7 +50,7 @@ function CreateJenisLayanan() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan tombol back */}
<Group mb="md">
<Button

View File

@@ -16,9 +16,10 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
useMantineTheme
} 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,28 +46,35 @@ function JenisLayanan() {
function ListJenisLayanan({ search }: { search: string }) {
const stateList = useProxy(layananonlineDesa.jenisLayanan);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = stateList.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
const theme = useMantineTheme();
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Stack py="lg" gap="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Layanan</Title>
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Daftar Jenis Layanan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -80,13 +88,34 @@ function ListJenisLayanan({ search }: { search: string }) {
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: '30%' }}>Nama Jenis Layanan</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '30%' }}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Nama Jenis Layanan
</Text>
</TableTh>
<TableTh style={{ width: '55%' }}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Deskripsi
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center" c={theme.black}>
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -94,16 +123,28 @@ function ListJenisLayanan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
<Text
fz="md"
fw={500}
lh={1.5}
c={theme.black}
truncate="end"
lineClamp={1}
>
{item.nama}
</Text>
</TableTd>
<TableTd style={{ width: '40%' }}>
<Text fz="sm" c="dimmed" lineClamp={2}>
{item.deskripsi || '-'}
</Text>
<TableTd style={{ width: '55%' }}>
<Text
fz="sm"
fw={500}
lh={1.5}
c={theme.black}
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</TableTd>
<TableTd style={{ width: '15%' }}>
<TableTd style={{ width: '15%' }} ta="center">
<Button
size="xs"
radius="md"
@@ -124,8 +165,8 @@ function ListJenisLayanan({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis layanan yang cocok
</Text>
</Center>
@@ -135,22 +176,80 @@ function ListJenisLayanan({ 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="md">
<Stack gap="xs">
<Box pl={5}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Nama Jenis Layanan
</Text>
<Text fz="sm" fw={500} lh={1.5} c={theme.black}>
{item.nama}
</Text>
</Box>
<Box pl={5}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Deskripsi
</Text>
<Text
fz="sm"
fw={500}
lh={1.5}
c={theme.black}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis layanan 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="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
color="blue"
radius="md"
/>
</Center>
)}
</Stack>
);
}

View File

@@ -105,7 +105,7 @@ function EditJenisPengaduan() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button

View File

@@ -47,7 +47,7 @@ function CreateJenisPengaduan() {
};
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">

View File

@@ -18,7 +18,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -48,12 +48,13 @@ function ListJenisPengaduan({ 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, page, totalPages, loading, load, } = state.findMany;
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const handleHapus = async () => {
if (selectedId) {
@@ -63,21 +64,23 @@ function ListJenisPengaduan({ search }: { search: string }) {
}
};
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">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Pengaduan</Title>
<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 Jenis Pengaduan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -92,13 +95,33 @@ function ListJenisPengaduan({ search }: { search: string }) {
</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: '60%' }}>Nama Jenis Pengaduan</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
<TableTh style={{ width: '60%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dimmed">
Nama Jenis Pengaduan
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dimmed">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dimmed">
Hapus
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -106,7 +129,7 @@ function ListJenisPengaduan({ 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>
@@ -147,7 +170,7 @@ function ListJenisPengaduan({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis pengaduan yang cocok
</Text>
</Center>
@@ -157,6 +180,63 @@ function ListJenisPengaduan({ 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 p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Jenis Pengaduan
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.nama}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`
)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
Hapus
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis pengaduan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
@@ -167,14 +247,13 @@ function ListJenisPengaduan({ 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"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
@@ -185,4 +264,4 @@ function ListJenisPengaduan({ search }: { search: string }) {
);
}
export default JenisPengaduan;
export default JenisPengaduan;

View File

@@ -1,7 +1,28 @@
'use client'
import { Box } from "@mantine/core";
import LayoutTabsLayananOnlineDesa from "./_lib/layoutTabs";
import { usePathname } from "next/navigation";
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 (
<LayoutTabsLayananOnlineDesa>
{children}

View File

@@ -41,7 +41,7 @@ function DetailPengaduanMasyarakat() {
const data = pengaduanState.pengaduanMasyarakat.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -55,7 +55,7 @@ function DetailPengaduanMasyarakat() {
{/* Card Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -19,8 +19,7 @@ import {
Title
} from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -46,6 +45,7 @@ function PengaduanMasyarakat() {
function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
@@ -55,56 +55,84 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
} = listState.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">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pengaduan Masyarakat</Title>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
{/* Section Header */}
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={1.2}>
Daftar Pengaduan Masyarakat
</Title>
</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%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Email</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Email</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Aksi</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.email}</Text>
<TableTd>
<Text fz="sm" c="dimmed" lh={1.5} truncate lineClamp={1}>
{item.email}
</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.nomorTelepon}</Text>
<TableTd>
<Text fz="sm" c="dimmed" lh={1.5} truncate lineClamp={1}>
{item.nomorTelepon}
</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
onClick={() =>
router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)
}
>
Detail
</Button>
@@ -114,8 +142,10 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pengaduan yang cocok</Text>
<Center py={{ base: 'sm', md: 'lg' }}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pengaduan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -123,7 +153,56 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<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="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.45}>{item.name}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Email</Text>
<Text fz="sm" c="dimmed" fw={500} lh={1.45}>{item.email}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
<Text fz="sm" c="dimmed" fw={500} lh={1.45}>{item.nomorTelepon}</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)
}
fullWidth
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pengaduan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -142,4 +221,4 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
);
}
export default PengaduanMasyarakat;
export default PengaduanMasyarakat;

View File

@@ -160,7 +160,7 @@ function EditProgramKreatifDesa() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button
variant="subtle"

View File

@@ -41,7 +41,7 @@ function DetailProgramKreatifDesa() {
const data = stateProgramKreatif.findUnique.data
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -53,7 +53,7 @@ function DetailProgramKreatifDesa() {
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -52,7 +52,7 @@ function CreateProgramKreatifDesa() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -18,7 +18,7 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import {
IconCash,
@@ -50,9 +50,10 @@ import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useDebouncedValue } from '@mantine/hooks';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
@@ -71,12 +72,13 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState);
const { data, loading, page, totalPages, load } = listState.findMany;
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -103,7 +105,7 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'xl' }}>
<Skeleton height={650} radius="md" />
</Stack>
);
@@ -111,35 +113,44 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" radius="md" shadow="sm" withBorder>
<Stack>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Box py={{ base: 'sm', md: 'xl' }}>
<Paper p="lg" radius="md" shadow="sm" withBorder>
<Stack gap="xl">
<Group justify="space-between" wrap="nowrap">
<Title order={4} lh={1.15}>
Daftar Program Kreatif Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
router.push('/admin/inovasi/program-kreatif-desa/create')
}
>
Tambah Baru
</Button>
</Group>
<Table highlightOnHover striped>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
striped
style={{ tableLayout: 'fixed', width: '100%' }}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
<Text ta="center" c="dimmed" py="lg">
Tidak ada data program kreatif desa yang tersedia
</Text>
@@ -150,7 +161,7 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
}
return (
<Box py={10}>
<Box py={{ base: 'sm', md: 'xl' }}>
<Paper
withBorder
bg={colors['white-1']}
@@ -159,58 +170,83 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
radius="md"
h={{ base: 'auto', md: 650 }}
>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Group justify="space-between" mb="xl" wrap="nowrap">
<Title order={4} lh={1.15}>
Daftar Program Kreatif Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
router.push('/admin/inovasi/program-kreatif-desa/create')
}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowY: 'auto' }}>
<Table highlightOnHover striped >
{/* Desktop Table */}
<Box visibleFrom="md" style={{ height: '100%', overflowY: 'auto' }}>
<Table
highlightOnHover
miw={0}
striped
style={{ tableLayout: 'fixed', width: '100%' }}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>
<Text lineClamp={1} fw={"bold"} fz="sm">Nama Program Kreatif Desa</Text>
<TableTh style={{ width: '5%', textAlign: 'center' }}>
<Text fz="xs" fw={600} lh={1.2}>
No
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="xs" fw={600} lh={1.2}>
Nama Program Kreatif Desa
</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>
<Text fz="xs" fw={600} lh={1.2}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>
<Text fz="xs" fw={600} lh={1.2}>
Ikon
</Text>
</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>
<Text fz="xs" fw={600} lh={1.2}>
Detail
</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
<TableTd ta="center" fz="sm" fw={500} lh={1.45}>
{index + 1}
</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Box w={150}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.slug}
</Text>
</Box>
<TableTd>
<Text fz="sm" fw={500} lh={1.45} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '10%', textAlign: 'center' }}>
<TableTd>
<Text fz="sm" c="dimmed" lh={1.45} lineClamp={1}>
{item.slug}
</Text>
</TableTd>
<TableTd ta="center">
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<TableTd ta="center">
<Button
size="xs"
radius="md"
@@ -221,7 +257,9 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
}
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
<Text ml={6} fz="sm" fw={500}>
Detail
</Text>
</Button>
</TableTd>
</TableTr>
@@ -229,7 +267,72 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.map((item, index) => (
<Paper key={item.id} p="md" withBorder radius="md">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Program Kreatif Desa
</Text>
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text fz="sm" fw={500} c="dimmed" lh={1.4} lineClamp={2}>
{item.slug}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Ikon
</Text>
<Box mt={4} ta="center">
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</Box>
</Box>
<Box ta="center" mt="sm">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)
}
>
<IconDeviceImac size={18} />
<Text ml={6} fz="sm" fw={500}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -238,8 +341,8 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
mt="xl"
mb="xl"
color="blue"
radius="md"
/>
@@ -248,4 +351,4 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
);
}
export default ProgramKreatifDesa;
export default ProgramKreatifDesa;

View File

@@ -38,7 +38,7 @@ function ListTarifLayanan({ search }: { search: string }) {
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 10000);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,

View File

@@ -96,15 +96,15 @@ function ListKontakDarurat({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<TableTd w={335}>
<Text fw={500} fz="md" lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<TableTd w={335}>
<Text fz="sm" lh={1.45} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<TableTd w={335}>
<Button
variant="light"
color="blue"

View File

@@ -110,10 +110,10 @@ function ListProgramKesehatan({ search }: { search: string }) {
{item.name}
</Text>
</TableTd>
<TableTd w={200}>
<TableTd>
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</TableTd>
<TableTd w={200}>
<TableTd>
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>

View File

@@ -18,7 +18,7 @@ import {
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -50,6 +50,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const router = useRouter();
const { data, page, totalPages, loading, load } = stateKategori.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const handleHapus = () => {
if (selectedId) {
@@ -60,8 +61,8 @@ function ListKategoriKegiatan({ search }: { search: string }) {
};
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
@@ -211,7 +212,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
<Box hiddenFrom="md">{renderMobileCards()}</Box>
</Paper>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
@@ -224,7 +225,6 @@ function ListKategoriKegiatan({ search }: { search: string }) {
radius="md"
/>
</Center>
)}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,7 +1,7 @@
'use client'
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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -29,10 +29,11 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
const router = useRouter();
const listState = useProxy(korupsiState.desaAntikorupsi);
const { data, page, totalPages, loading, load } = listState.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];

View File

@@ -2,7 +2,7 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import {
Box,
Button,
@@ -47,16 +47,14 @@ interface ListRespondenProps {
function ListResponden({ search }: ListRespondenProps) {
const state = useProxy(indeksKepuasanState.responden);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = (data || []).filter((item) => {
const keyword = search.toLowerCase();
return item.name.toLowerCase().includes(keyword);
});
const filteredData = data || [];
if (loading || !data) {
return (

View File

@@ -196,6 +196,7 @@ function ListMediaSosial({ search }: { search: string }) {
{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 Media Sosial / Kontak</Text>
<Text fw={500} fz="sm" lh={1.45}>
@@ -221,37 +222,38 @@ function ListMediaSosial({ search }: { search: string }) {
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()}
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
<Text
fz="sm"
c="blue"
truncate
<Box>
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
{item.iconUrl || item.noTelp || '-'}
</Text>
</a>
</Box>
<Group mt="sm" justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
}
>
Detail
</Button>
</Group>
<Text
fz="sm"
c="blue"
truncate
>
{item.iconUrl || item.noTelp || '-'}
</Text>
</a>
</Box>
<Group mt="sm" justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (

View File

@@ -1,7 +1,7 @@
'use client'
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -29,22 +29,18 @@ function Page() {
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
style={{fontSize: 15, fontWeight: "bold"}}
c="green"
variant="light"
radius="md"
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Group justify="space-between">
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
<Button
style={{ fontSize: 15, fontWeight: "bold" }}
c="green"
variant="light"
radius="md"
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
</Group>
{dataArray.map((item) => (
<Paper key={item.id} p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: "sm", md: 100 }}>

View File

@@ -84,7 +84,9 @@ function DetailProgramInovasi() {
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
<Box pl={5}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
</Box>
</Box>
<Box>

View File

@@ -34,8 +34,8 @@ function ListProgramInovasi({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
@@ -144,9 +144,7 @@ function ListProgramInovasi({ search }: { search: string }) {
{/* Description */}
<Box>
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" c="gray.7" lineClamp={2}>
{item.description || '-'}
</Text>
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }} fz="sm" c="gray.7" lineClamp={2} />
</Box>
{/* Link */}

View File

@@ -132,7 +132,7 @@ export default function EditDataLingkunganDesa() {
};
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} />

View File

@@ -79,7 +79,7 @@ function DetailDataLingkunganDesa() {
const data = stateDataLingkungan.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Back Button */}
<Button
variant="subtle"
@@ -93,7 +93,7 @@ function DetailDataLingkunganDesa() {
{/* Main Card */}
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -131,7 +131,9 @@ function DetailDataLingkunganDesa() {
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
<Box pl={5}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
</Box>
{/* Action Buttons */}

View File

@@ -49,7 +49,7 @@ function CreateDataLingkunganDesa() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button

View File

@@ -1,30 +1,65 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* 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
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled,
IconDeviceImacCog, IconDroplet, IconHome, IconHomeEco, IconLeaf,
IconAlertTriangle,
IconAmbulance,
IconBuilding,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDeviceImacCog,
IconDroplet,
IconFiretruck,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconPlus,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
IconTrashFilled, IconTree, IconTrendingUp, IconTrophy, IconTruckFilled
IconRecycle,
IconScale,
IconSchool,
IconSearch,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import dataLingkunganDesaState from '../../_state/lingkungan/data-lingkungan-desa';
import { useDebouncedValue } from '@mantine/hooks';
function DataLingkunganDesa() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<Box py={{ base: 'md', md: 'lg' }} px={{ base: 'sm', md: 0 }}>
<HeaderSearch
title='Data Lingkungan Desa'
placeholder='Cari data lingkungan...'
@@ -38,15 +73,16 @@ function DataLingkunganDesa() {
}
function ListDataLingkunganDesa({ search }: { search: string }) {
const listState = useProxy(dataLingkunganDesaState)
const { data, loading, page, totalPages, load } = listState.findMany
const listState = useProxy(dataLingkunganDesaState);
const { data, loading, page, totalPages, load } = listState.findMany;
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
useEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -64,12 +100,22 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
polisi: IconShieldFilled,
ambulans: IconAmbulance,
pemadam: IconFiretruck,
rumahSakit: IconHospital,
bangunan: IconBuilding,
darurat: IconAlertTriangle
};
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="md">
<Skeleton height={600} radius="md" />
</Stack>
);
@@ -77,46 +123,66 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py={10}>
<Paper withBorder shadow="md" radius="md" p="lg" bg={colors['white-1']}>
<Box py="md">
<Paper withBorder shadow="md" radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
<Stack>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Lingkungan Desa</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}>
<Title order={4} lh={1.2}>
Daftar Data Lingkungan Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}
>
Tambah Baru
</Button>
</Group>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Data Lingkungan Desa</TableTh>
<TableTh>Jumlah</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Center py={20}>
<Text c="dimmed">Tidak ada data lingkungan desa yang tersedia</Text>
<Box visibleFrom="md">
<Table highlightOnHover miw={0} style={{ tableLayout: 'fixed', width: '100%' }}>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama Data Lingkungan Desa</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah</TableTh>
<TableTh style={{ width: '15%' }}>Ikon</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
<Center py="xl">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
Tidak ada data lingkungan desa yang tersedia
</Text>
</Center>
</Stack>
</Paper>
</Box >
</Box>
);
}
return (
<Box py={10}>
<Paper withBorder shadow="md" radius="md" bg={colors['white-1']} p="lg">
<Box py="md">
<Paper withBorder shadow="md" radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Lingkungan Desa</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}>
<Title order={4} lh={1.2}>
Daftar Data Lingkungan Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}
>
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: '5%', textAlign: 'center' }}>No</TableTh>
@@ -129,21 +195,21 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd ta="center">{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">± {item.jumlah}</Text>
<Text fz="sm" c="dimmed" lh={1.5}>
± {item.jumlah}
</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 22 })}
</Box>
)}
{iconMap[item.icon] && <Box title={item.icon}>{React.createElement(iconMap[item.icon], { size: 22 })}</Box>}
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<TableTd ta="center">
<Button
size="xs"
radius="md"
@@ -160,7 +226,62 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.map((item, index) => (
<Paper key={item.id} withBorder radius="md" p="sm" bg={colors['white-1']}>
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Data
</Text>
<Text fz="sm" fw={500} lh={1.4} truncate="end">
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah
</Text>
<Text fz="sm" fw={500} lh={1.4}>
± {item.jumlah}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Ikon
</Text>
{iconMap[item.icon] && <Box title={item.icon}>{React.createElement(iconMap[item.icon], { size: 22 })}</Box>}
</Box>
<Center>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}
>
Detail
</Button>
</Center>
</Stack>
</Paper>
))}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -178,4 +299,5 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
</Box>
);
}
export default DataLingkunganDesa;
export default DataLingkunganDesa;

View File

@@ -59,43 +59,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<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",
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
<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}
</span>
</TabsTab>
))}
</TabsList>
</ScrollArea>
</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}>

View File

@@ -96,7 +96,7 @@ export default function EditContohKegiatanDesaDarmasaba() {
};
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">

View File

@@ -1,5 +1,5 @@
'use client'
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -25,13 +25,10 @@ function Page() {
return (
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Group justify="space-between" align="center" mb={{ base: 'md', md: 'lg' }}>
<Title order={3} fw={600}>
Preview Contoh Kegiatan Di Desa Darmasaba
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
@@ -46,8 +43,7 @@ function Page() {
>
Edit
</Button>
</GridCol>
</Grid>
</Group>
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">

View File

@@ -1,7 +1,28 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layouTabs";
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 (
<LayoutTabs>
{children}

View File

@@ -81,7 +81,7 @@ export default function EditMateriEdukasiYangDiberikan() {
};
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} />

View File

@@ -1,5 +1,5 @@
'use client'
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -25,11 +25,8 @@ function Page() {
return (
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Group align="center" justify='space-between' mb={{ base: 'md', md: 'lg' }}>
<Title order={3} fw={600}>Preview Materi Edukasi Yang Diberikan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
@@ -42,8 +39,7 @@ function Page() {
>
Edit
</Button>
</GridCol>
</Grid>
</Group>
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">

View File

@@ -84,7 +84,7 @@ export default function EditTujuanEdukasiLingkungan() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button
variant="subtle"

View File

@@ -1,5 +1,5 @@
'use client'
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -25,25 +25,21 @@ function Page() {
return (
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} fw={600}>Preview Tujuan Edukasi Lingkungan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')
}
>
Edit
</Button>
</GridCol>
</Grid>
<Group align="center" justify='space-between' mb={{ base: 'md', md: 'lg' }}>
<Title order={3} fw={600}>Preview Tujuan Edukasi Lingkungan</Title>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')
}
>
Edit
</Button>
</Group>
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">

View File

@@ -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 { IconClipboardList, IconTags } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -59,35 +59,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal */}
<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",
}}
>
{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>
<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>
{/* ✅ Panel dengan gaya kartu */}
{tabs.map((tab, i) => (

View File

@@ -83,7 +83,7 @@ function EditKategoriKegiatan() {
if (loading) return <Text>Loading...</Text>;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md" align="center">
<Button variant="subtle" p="xs" radius="md" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -39,7 +39,7 @@ function CreateKategoriKegiatan() {
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md" align="center">
<Button variant="subtle" p="xs" radius="md" onClick={() => router.back()}>

View File

@@ -18,7 +18,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -28,7 +28,7 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import gotongRoyongState from '../../../_state/lingkungan/gotong-royong';
function KategoriKegiatan() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
@@ -44,10 +44,11 @@ function KategoriKegiatan() {
}
function ListKategoriKegiatan({ search }: { search: string }) {
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -55,52 +56,76 @@ function ListKategoriKegiatan({ search }: { search: string }) {
totalPages,
loading,
load,
} = stateKategori.findMany
} = stateKategori.findMany;
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateKategori.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
}
};
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: '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">
<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="md">
<Title order={4}>Daftar Kategori Kegiatan</Title>
<Title order={4} lh={1.2}>
Daftar Kategori Kegiatan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan/create')}
onClick={() =>
router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan/create')
}
>
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: '60%' }}>Nama Kategori</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Delete</TableTh>
<TableTh style={{ width: '60%' }}>
<Text fz="sm" fw={600} lh={1.2} ta="left">
Nama Kategori
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.2} ta="center">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.2} ta="center">
Delete
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -108,21 +133,27 @@ function ListKategoriKegiatan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.nama}
</Text>
</TableTd>
<TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() => router.push(`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`)}
onClick={() =>
router.push(
`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`
)
}
>
Edit
</Button>
</TableTd>
<TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
@@ -130,8 +161,8 @@ function ListKategoriKegiatan({ search }: { search: string }) {
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
setSelectedId(item.id);
setModalHapus(true);
}}
>
Hapus
@@ -142,8 +173,10 @@ function ListKategoriKegiatan({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text c="dimmed">Tidak ada kategori kegiatan yang cocok</Text>
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada kategori kegiatan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -151,6 +184,63 @@ function ListKategoriKegiatan({ 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="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Kategori
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.nama}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`
)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
Hapus
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada kategori kegiatan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
@@ -179,4 +269,4 @@ function ListKategoriKegiatan({ search }: { search: string }) {
);
}
export default KategoriKegiatan;
export default KategoriKegiatan;

View File

@@ -162,7 +162,7 @@ export default function EditKegiatanDesa() {
};
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} />

View File

@@ -41,7 +41,7 @@ function DetailKegiatanDesa() {
const data = kegiatanDesaState.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -55,7 +55,7 @@ function DetailKegiatanDesa() {
{/* Container Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -85,13 +85,17 @@ function DetailKegiatanDesa() {
{/* Deskripsi Singkat */}
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} />
<Box pl={5}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} />
</Box>
</Box>
{/* Deskripsi Lengkap */}
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} />
<Box pl={5}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} />
</Box>
</Box>
{/* Kategori */}

View File

@@ -86,7 +86,7 @@ function CreateKegiatanDesa() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button

View File

@@ -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';
@@ -45,6 +45,7 @@ function KegiatanDesa() {
function ListKegiatanDesa({ search }: { search: string }) {
const listState = useProxy(gotongRoyongState.kegiatanDesa);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -55,24 +56,26 @@ function ListKegiatanDesa({ search }: { search: string }) {
} = listState.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">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kegiatan Desa</Title>
<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}>
Daftar Kegiatan Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -84,14 +87,39 @@ function ListKegiatanDesa({ search }: { search: string }) {
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: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Lokasi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '30%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Judul
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Kategori
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Lokasi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -99,17 +127,36 @@ function ListKegiatanDesa({ 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}
c="dark"
>
{item.judul}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
<Text
fz="sm"
fw={500}
lh={1.5}
c="dark"
>
{item.kategoriKegiatan?.nama || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm">{item.lokasi}</Text>
<Text
fz="sm"
fw={500}
lh={1.5}
c="dark"
>
{item.lokasi}
</Text>
</TableTd>
<TableTd>
<Button
@@ -133,7 +180,7 @@ function ListKegiatanDesa({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
<Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada kegiatan desa yang cocok dengan pencarian
</Text>
</Center>
@@ -143,6 +190,64 @@ function ListKegiatanDesa({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile: Card List */}
<Stack hiddenFrom="md" gap="xs">
{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} c="dark">
Judul
</Text>
<Text fz="sm" fw={500} lh={1.4} c="dark" truncate="end">
{item.judul}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Kategori
</Text>
<Text fz="sm" fw={500} lh={1.4} c="dark">
{item.kategoriKegiatan?.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4} c="dark">
Lokasi
</Text>
<Text fz="sm" fw={500} lh={1.4} c="dark">
{item.lokasi}
</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/lingkungan/gotong-royong/kegiatan-desa/${item.id}`
)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada kegiatan desa yang cocok dengan pencarian
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
<Pagination
@@ -162,4 +267,4 @@ function ListKegiatanDesa({ search }: { search: string }) {
);
}
export default KegiatanDesa;
export default KegiatanDesa;

View File

@@ -1,6 +1,28 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs 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 (
<LayoutTabs>
{children}

View File

@@ -59,43 +59,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<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",
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
<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}
</span>
</TabsTab>
))}
</TabsList>
</ScrollArea>
</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}>

View File

@@ -77,7 +77,7 @@ function EditBentukKonservasiBerdasarkanAdat() {
};
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">

Some files were not shown because too many files have changed in this diff Show More