Added comprehensive QC reports and fix summaries for: - Desa (Berita, Potensi, Profil, Layanan, Penghargaan, Pengumuman) - Kesehatan (Posyandu) - Landing Page (APBDes, SDGS, Anti-Korupsi, Profil, Prestasi) - PPID (Daftar Informasi, Dasar Hukum, IKM, Permohonan, Struktur, Visi Misi)
28 KiB
Quality Control Report - Gallery Desa Admin
Lokasi: /src/app/admin/(dashboard)/desa/gallery/
Tanggal QC: 25 Februari 2026
Status: ⚠️ Needs Improvement (ada issue critical data loss risk)
📋 Ringkasan Eksekutif
Halaman Gallery Desa (Foto & Video) memiliki implementasi yang cukup baik dengan CRUD lengkap, upload gambar, YouTube embed, dan state management terstruktur. Namun ditemukan 18 issue dengan rincian:
- 🔴 High Priority: 5 issue
- 🟡 Medium Priority: 8 issue
- 🟢 Low Priority: 5 issue
Overall Score: 6/10 - Needs Improvement
📁 Struktur File yang Diperiksa
/src/app/admin/(dashboard)/desa/gallery/
├── layout.tsx
├── lib/
│ ├── layoutTabs.tsx # Tab navigation Foto/Video
│ ├── youtube-utils.ts # YouTube URL conversion utilities
│ └── youtubeEmbed.tsx # Reusable embed component (UNUSED)
├── foto/
│ ├── page.tsx # List foto dengan search & pagination
│ ├── create/
│ │ └── page.tsx # Upload foto dengan dropzone
│ └── [id]/
│ ├── page.tsx # Detail foto
│ └── edit/
│ └── page.tsx # Edit foto (replace image)
└── video/
├── page.tsx # List video dengan search & pagination
├── create/
│ └── page.tsx # Add video YouTube dengan embed preview
└── [id]/
├── page.tsx # Detail video
└── edit/
└── page.tsx # Edit video
File Terkait:
- State:
/src/app/admin/(dashboard)/_state/desa/gallery.ts - API:
/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/(7 files) - API:
/src/app/api/[[...slugs]]/_lib/desa/gallery/video/(6 files) - Schema:
/prisma/schema.prisma(ModelGalleryFoto&GalleryVideo)
🔴 HIGH PRIORITY ISSUES
1. Schema - deletedAt @default(now()) (CRITICAL BUG)
File: prisma/schema.prisma
model GalleryFoto {
id String @id @default(cuid())
name String
deletedAt DateTime @default(now()) // ❌ CRITICAL BUG
isActive Boolean @default(true)
}
model GalleryVideo {
id String @id @default(cuid())
name String
deletedAt DateTime @default(now()) // ❌ CRITICAL BUG
isActive Boolean @default(true)
}
Dampak:
- Setiap record baru langsung ter-mark sebagai deleted saat dibuat
- Query dengan filter
deletedAt: nulltidak akan dapat data baru - Soft delete logic tidak bekerja sama sekali
- Data inconsistency antara
deletedAt(set) danisActive(true)
Severity: 🔴 CRITICAL - Ini adalah bug yang sama seperti di Profil Desa dan Pengumuman
Solusi:
model GalleryFoto {
id String @id @default(cuid())
name String
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
model GalleryVideo {
id String @id @default(cuid())
name String
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
Migration Required:
bunx prisma migrate dev --name fix_deleted_at_default
# atau
bunx prisma db push
# Data cleanup
UPDATE "GalleryFoto" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "GalleryVideo" SET "deletedAt" = NULL WHERE "isActive" = true;
2. API - File Orphaning Saat Create Gagal
File: src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
// Line 78-88
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
FotoState.create.form.imagesId = uploaded.id;
await FotoState.create.create(); // ❌ Jika ini gagal, file sudah ter-upload
Dampak:
- File ter-upload ke server tapi gallery tidak terbuat
- Orphaned files menumpuk di database dan filesystem
- Storage waste, tidak ada cleanup mechanism
Severity: 🔴 HIGH - Data integrity issue
Solusi:
Option A - Transaction di API:
// Di API create.ts
try {
// Validate fileStorage exists first
const fileStorage = await prisma.fileStorage.findUnique({
where: { id: body.imagesId }
});
if (!fileStorage) {
return Response.json({
success: false,
message: "File tidak ditemukan"
}, { status: 404 });
}
const gallery = await prisma.galleryFoto.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imagesId: body.imagesId,
}
});
return { success: true, data: gallery };
} catch (error) {
// Rollback file jika create gagal
if (body.imagesId) {
await prisma.fileStorage.delete({ where: { id: body.imagesId } }).catch(() => {});
}
throw error;
}
Option B - Cleanup di Frontend:
try {
const uploaded = await uploadFile(file);
const result = await createGallery({ ...imagesId: uploaded.id });
if (!result.success) {
// Cleanup orphaned file
await deleteFile(uploaded.id);
throw new Error('Create gallery failed');
}
} catch (error) {
toast.error('Gagal membuat gallery');
}
3. API - Old File Dihapus Sebelum Update Confirmed
File: src/app/api/[[...slugs]]/_lib/desa/gallery/foto/updt.ts
// Line 47-58
if (existing.imagesId && existing.imagesId !== body.imagesId) {
const oldImage = existing.imageGalleryFoto;
if (oldImage) {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath); // ❌ File dihapus DULU
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
}
}
// Baru update data
const updated = await prisma.galleryFoto.update({
where: { id },
data: { ... }
});
Dampak:
- Jika
prisma.galleryFoto.update()gagal, old file sudah terhapus - DATA LOSS - Gallery tidak punya image sama sekali
- Tidak ada rollback mechanism
Severity: 🔴 HIGH - Data loss risk
Solusi:
// Update data DULU, baru hapus old file
const updated = await prisma.galleryFoto.update({
where: { id },
data: {
name: body.name,
deskripsi: body.deskripsi,
imagesId: body.imagesId,
},
include: { imageGalleryFoto: true }
});
// Hapus old file SETELAH update berhasil
if (existing.imagesId && existing.imagesId !== body.imagesId) {
const oldImage = existing.imageGalleryFoto;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (error) {
console.error('Failed to delete old file:', error);
// Log error tapi tidak rollback karena update sudah berhasil
}
}
}
4. API - Tidak Ada Authentication/Authorization
File: Semua API endpoints di /src/app/api/[[...slugs]]/_lib/desa/gallery/
export default async function fotoCreate(context: Context) {
// ❌ Tidak ada validasi session/user
const body = await context.body;
// Langsung proses create
await prisma.galleryFoto.create({ ... });
}
Dampak:
- Siapa saja bisa upload/delete foto/video jika tahu endpoint
- Tidak ada audit trail siapa yang upload/delete
- Security risk untuk production
Severity: 🔴 HIGH - Security vulnerability
Solusi:
import { getSession } from '@/lib/auth';
export default async function fotoCreate(context: Context) {
const session = await getSession();
if (!session || !session.user) {
return Response.json({
success: false,
message: "Unauthorized"
}, { status: 401 });
}
// Check role/permission jika perlu
if (!session.user.menuIds?.includes('gallery')) {
return Response.json({
success: false,
message: "Forbidden"
}, { status: 403 });
}
const body = await context.body;
// ... lanjut proses
}
5. API - Tidak Ada Input Validation
File: src/app/api/[[...slugs]]/_lib/desa/gallery/foto/create.ts
// Line 13-23
await prisma.galleryFoto.create({
data: {
name: body.name, // ❌ Tidak ada validasi length
deskripsi: body.deskripsi, // ❌ Tidak ada sanitasi XSS
imagesId: body.imagesId, // ❌ Tidak cek apakah FileStorage ada
},
});
Dampak:
- User bisa input name sangat panjang (bisa break UI/database)
- XSS attack via
deskripsifield (rich text editor) - Bisa create gallery dengan
imagesIdyang tidak valid
Severity: 🔴 HIGH - Security & data integrity
Solusi:
// Validasi input
const { name, deskripsi, imagesId } = await context.body;
// Check length
if (!name || name.length > 255) {
return Response.json({
success: false,
message: "Name maksimal 255 karakter"
}, { status: 400 });
}
// Check duplikasi
const existing = await prisma.galleryFoto.findFirst({
where: { name, isActive: true }
});
if (existing) {
return Response.json({
success: false,
message: "Name sudah digunakan"
}, { status: 400 });
}
// Check fileStorage exists
const fileStorage = await prisma.fileStorage.findUnique({
where: { id: imagesId }
});
if (!fileStorage) {
return Response.json({
success: false,
message: "File tidak ditemukan"
}, { status: 404 });
}
// Sanitize HTML (gunakan library seperti DOMPurify di server)
const sanitizedDeskripsi = sanitizeHtml(deskripsi);
// Create
const gallery = await prisma.galleryFoto.create({
data: {
name,
deskripsi: sanitizedDeskripsi,
imagesId,
}
});
🟡 MEDIUM PRIORITY ISSUES
6. UI - Dead Code (youtubeEmbed.tsx Tidak Digunakan)
File: src/app/admin/(dashboard)/desa/gallery/lib/youtubeEmbed.tsx
// Component ini TIDAK digunakan di mana pun
export function YoutubeEmbed({ url, ... }: Props) {
// ... component code
}
Dampak:
- Dead code menumpuk (120+ baris tidak digunakan)
- Confusing untuk developer baru
- Maintenance overhead
Severity: 🟡 MEDIUM - Code quality issue
Solusi:
- Option A: Hapus file ini jika memang tidak diperlukan
- Option B: Gunakan component ini di semua halaman (create, edit, detail) untuk konsistensi
Recommendation: Hapus, karena setiap halaman sudah implementasi iframe manual dengan cara berbeda.
7. UI - Inconsistent Styling Foto vs Video
File: foto/page.tsx vs video/page.tsx
// foto/page.tsx - Line 58
<Box p={{ base: 'md', md: 'lg' }}> // ✅ Responsive padding
// video/page.tsx - Line 60
<Box py={20}> // ❌ Hardcoded padding
Dampak: Inconsistent spacing antara foto dan video pages.
Severity: 🟡 MEDIUM - UX inconsistency
Solusi:
// video/page.tsx
<Box p={{ base: 'md', md: 'lg' }}> // ✅ Konsisten dengan foto
8. UI - Memory Leak Potential (createObjectURL)
File: src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
// Line 59-62
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewImage(url);
}
}, [file]);
// Line 47-52
const resetForm = () => {
FotoState.create.form = { name: '', deskripsi: '', imagesId: '' };
setPreviewImage(null);
setFile(null);
// ❌ URL.revokeObjectURL() tidak dipanggil
};
Dampak:
- Memory leak jika user upload banyak gambar tanpa refresh
- Browser bisa crash setelah banyak createObjectURL tidak di-cleanup
Severity: 🟡 MEDIUM - Performance issue
Solusi:
// Cleanup saat unmount atau file berubah
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewImage(url);
return () => {
URL.revokeObjectURL(url); // ✅ Cleanup
};
}
}, [file]);
// Cleanup saat reset
const resetForm = () => {
if (previewImage) {
URL.revokeObjectURL(previewImage); // ✅ Cleanup
}
FotoState.create.form = { name: '', deskripsi: '', imagesId: '' };
setPreviewImage(null);
setFile(null);
};
9. State - Error Handling Tidak Konsisten
File: src/app/admin/(dashboard)/_state/desa/gallery.ts
// Line 39-53 (foto.create)
async create() {
try {
const res = await ApiFetch.api.desa.gallery.foto["create"].post(foto.create.form);
if (res.status === 200) {
foto.findMany.load();
return toast.success("Foto berhasil disimpan!");
}
return toast.error("Gagal menyimpan foto");
} catch (error) {
console.log((error as Error).message);
// ❌ Error di-catch tapi tidak ada toast error notification
}
}
// Line 91-107 (foto.findUnique)
async load(id: string) {
try {
const res = await fetch(`/api/desa/gallery/foto/${id}`);
if (res.ok) {
const data = await res.json();
foto.findUnique.data = data.data ?? null;
}
} catch (error) {
console.error("Error fetching foto:", error);
// ❌ Tidak ada error toast notification
}
}
// Line 205-227 (foto.findRecent)
async load() {
try {
// ...
} catch (error) {
console.error("Gagal fetch foto recent:", error);
// ❌ Tidak ada error toast notification
}
}
Dampak:
- User tidak tahu ada error (silent failure)
- UX buruk (loading forever tanpa feedback)
- Sulit debug production issues
Severity: 🟡 MEDIUM - UX issue
Solusi:
async create() {
try {
const res = await ApiFetch.api.desa.gallery.foto["create"].post(foto.create.form);
if (res.status === 200) {
foto.findMany.load();
return toast.success("Foto berhasil disimpan!");
}
return toast.error("Gagal menyimpan foto");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('Create foto failed:', errorMessage);
toast.error(`Gagal menyimpan foto: ${errorMessage}`); // ✅ Show error
}
}
10. State - findMany.load() Dipanggil Tanpa Parameter
File: src/app/admin/(dashboard)/_state/desa/gallery.ts
// Line 47 (foto.create)
async create() {
// ...
if (res.status === 200) {
foto.findMany.load(); // ❌ Tanpa parameter (default page=1, limit=10)
return toast.success("Foto berhasil disimpan!");
}
}
// Line 119 (foto.delete)
async byId(id: string) {
// ...
if (response.ok) {
toast.success(result.message || "Foto berhasil dihapus");
await foto.findMany.load(); // ❌ Tanpa parameter
}
}
Dampak:
- Jika user di page 5, setelah create/delete refresh ke page 1
- User bingung kenapa data hilang (padahal masih ada, cuma page berubah)
Severity: 🟡 MEDIUM - UX issue
Solusi:
// Simpan current pagination state
let currentPage = 1;
let currentLimit = 10;
let currentSearch = '';
// Set parameter saat load
async load(page = 1, limit = 10, search = '') {
currentPage = page;
currentLimit = limit;
currentSearch = search;
// ... load data
}
// Gunakan current state saat refresh
async create() {
// ...
if (res.status === 200) {
await foto.findMany.load(currentPage, currentLimit, currentSearch); // ✅ Pass current params
toast.success("Foto berhasil disimpan!");
}
}
11. API - Video Search Tidak Include deskripsi
File: src/app/api/[[...slugs]]/_lib/desa/gallery/video/find-many.ts
// Line 18-21
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } }
// ❌ deskripsi tidak di-include
];
}
Bandingkan dengan Foto:
// foto/find-many.ts - Line 20-26
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } } // ✅ Include deskripsi
];
}
Dampak:
- User tidak bisa search video berdasarkan deskripsi
- Inconsistent behavior antara foto dan video
Severity: 🟡 MEDIUM - Feature inconsistency
Solusi:
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } } // ✅ Add deskripsi
];
}
12. UI - Skeleton Height Terlalu Besar
File: src/app/admin/(dashboard)/desa/gallery/video/page.tsx
// Line 73-77
if (loading || !data) {
return (
<Stack py={20}>
<Skeleton height={600} radius="md" /> // ❌ Terlalu besar
</Stack>
);
}
Dampak: Skeleton mengambil hampir seluruh layar, UX buruk.
Severity: 🟡 MEDIUM - UX issue
Solusi:
<Skeleton height={200} radius="md" /> // ✅ Lebih reasonable
13. UI - Duplicate convertToEmbedUrl Function
File: src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
// Line 106-118
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
return youtubeUrl;
}
}
Padahal sudah ada di: lib/youtube-utils.ts
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
);
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}
Dampak:
- Duplicate code (violation DRY principle)
- Logic berbeda (page.tsx hanya support watch URL, utils.ts support multiple formats)
- Maintenance overhead
Severity: 🟡 MEDIUM - Code quality issue
Solusi:
// Import dari utils
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
// Gunakan function yang sudah ada
const embedLink = convertYoutubeUrlToEmbed(data.linkVideo);
14. Utils - YouTube Shorts URL Tidak Disupport
File: src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
);
// ❌ Regex tidak support youtube.com/shorts/VIDEO_ID
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}
Dampak: User tidak bisa input YouTube Shorts URL (format populer).
Severity: 🟡 MEDIUM - Feature gap
Solusi:
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
);
// ✅ Added shorts\/ support
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}
🟢 LOW PRIORITY ISSUES
15. UI - Redundant Variable (filteredData)
File: src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
// Line 78-79
const filteredData = data || [];
// ❌ Variable ini redundant, data sudah difilter di backend
Dampak: Minor code clutter.
Severity: 🟢 LOW - Code cleanliness
Solusi: Hapus variable, gunakan langsung data || [].
16. UI - useEffect Redundant di layoutTabs.tsx
File: src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
// Line 35-40
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
// ❌ Redundant karena sudah ada logic serupa di handleTabChange
Dampak: Minor performance overhead.
Severity: 🟢 LOW - Code quality
Solusi: Hapus useEffect jika tidak diperlukan.
17. State - findRecent Tidak Digunakan di UI
File: src/app/admin/(dashboard)/_state/desa/gallery.ts
// Line 205-227
foto: {
findRecent: {
loading: false,
data: [] as any[],
async load() {
// ... fetch recent photos
}
}
}
Dampak: Dead code di state management.
Severity: 🟢 LOW - Code cleanliness
Solusi:
- Option A: Hapus jika memang tidak diperlukan
- Option B: Implementasi di UI (misal: widget "Recent Photos" di dashboard)
18. State - Mix State Mutation dan Return Value
File: src/app/admin/(dashboard)/_state/desa/gallery.ts
// Line 138-203 (foto.update)
async load(id: string) {
// ... fetch GET
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = { ... };
return data; // ❌ Mix mutation + return value (confusing API)
}
}
Dampak: Confusing API, tidak jelas apakah caller harus gunakan return value atau akses state langsung.
Severity: 🟢 LOW - Code quality
Solusi:
// Option A: Hanya mutation (recommended)
async load(id: string) {
// ... fetch GET
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = { ... };
// No return value
}
}
// Usage
await foto.update.load(id);
const formData = foto.update.form; // Akses dari state
// Option B: Hanya return value
async load(id: string) {
// ... fetch GET
if (result?.success) {
return result.data;
}
return null;
}
// Usage
const data = await foto.update.load(id);
✅ YANG SUDAH BAIK
Schema:
- ✅ Relasi GalleryFoto ke FileStorage sudah benar
- ✅ Kedua model memiliki soft delete fields (
deletedAt,isActive) - ✅ Audit trail dengan
createdAtdanupdatedAt
API:
- ✅ CRUD lengkap untuk Foto dan Video
- ✅ Pagination support dengan
page,limit - ✅ Search functionality (foto: name + deskripsi, video: name only)
- ✅ Soft delete di-support via
isActiveflag di find-many - ✅ File cleanup saat delete foto (hapus filesystem + database)
- ✅ Error handling ada di semua endpoints
- ✅ Response format konsisten:
{ success, message, data }
UI/UX:
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation (name, deskripsi, image required)
- ✅ Dropzone untuk upload gambar dengan preview
- ✅ File size limit (5MB) dan format validation
- ✅ Rich text editor untuk deskripsi
- ✅ YouTube URL conversion dengan embed preview
- ✅ Search dengan debounce (1000ms)
- ✅ Modal konfirmasi hapus
- ✅ Empty state message
- ✅ Reset form functionality
State Management:
- ✅ Valtio proxy untuk global state
- ✅ Separate state untuk foto dan video
- ✅ CRUD operations lengkap
- ✅ Form validation dengan Zod
- ✅ Pagination state management
- ✅ Loading states
Utilities:
- ✅ YouTube URL conversion support multiple formats (watch, embed, youtu.be)
- ✅ Reusable component pattern (youtubeEmbed.tsx - meski tidak digunakan)
📊 Metrics
| Aspek | Score | Keterangan |
|---|---|---|
| Schema Design | 6/10 | Good structure, tapi ada critical bug di deletedAt |
| API Design | 6/10 | RESTful, tapi tidak ada auth & validation |
| API Security | 4/10 | Tidak ada authentication, XSS risk |
| UI/UX | 7.5/10 | Responsive, comprehensive features |
| State Management | 6.5/10 | Valtio works well, inconsistency di error handling |
| Code Quality | 6/10 | Dead code, duplicate code, memory leak potential |
Overall Score: 6/10 - Needs Improvement
🎯 Action Plan
Week 1 (Critical Fixes) 🔴
- URGENT: Fix
deletedAt @default(now())di schema - URGENT: Fix file orphaning saat create gagal
- URGENT: Fix old file delete sebelum update confirmed
- URGENT: Tambahkan authentication di semua API endpoints
- URGENT: Tambahkan input validation di API
Week 2 (High Priority) 🟡
- Tambahkan rollback mechanism untuk operasi file
- Fix error handling konsisten (semua catch show toast)
- Fix
findMany.load()pass current pagination params - Tambahkan video search include deskripsi
- Fix memory leak (createObjectURL cleanup)
Week 3 (Polish) 🟢
- Hapus dead code (youtubeEmbed.tsx, findRecent)
- Konsistensi styling foto vs video pages
- Hapus duplicate convertToEmbedUrl function
- Tambahkan support YouTube Shorts URL
- Fix skeleton height
- Fix redundant useEffect di layoutTabs
📝 Technical Notes
Database Migration:
Fix deletedAt default:
# Generate migration
bunx prisma migrate dev --name fix_gallery_deleted_at
# Atau jika tidak pakai migrate
bunx prisma db push
# Data cleanup
UPDATE "GalleryFoto" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "GalleryVideo" SET "deletedAt" = NULL WHERE "isActive" = true;
File Orphan Prevention:
Implementasi transaction pattern di API:
// Di API create.ts
export default async function fotoCreate(context: Context) {
try {
// Validate fileStorage exists
const fileStorage = await prisma.fileStorage.findUnique({
where: { id: body.imagesId }
});
if (!fileStorage) {
return Response.json({
success: false,
message: "File tidak ditemukan"
}, { status: 404 });
}
// Create gallery dengan transaction
const gallery = await prisma.galleryFoto.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imagesId: body.imagesId,
}
});
return { success: true, data: gallery };
} catch (error) {
// Rollback file jika create gagal
if (body.imagesId) {
await prisma.fileStorage.delete({
where: { id: body.imagesId }
}).catch(() => {});
}
throw error;
}
}
Memory Leak Prevention:
// Cleanup createObjectURL
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewImage(url);
return () => {
URL.revokeObjectURL(url); // ✅ Cleanup
};
}
}, [file]);
// Cleanup saat reset
const resetForm = () => {
if (previewImage) {
URL.revokeObjectURL(previewImage);
}
// ...
};
YouTube Shorts Support:
// youtube-utils.ts
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
);
return videoIdMatch
? `https://www.youtube.com/embed/${videoIdMatch[1]}`
: null;
}
// Test cases
convertYoutubeUrlToEmbed('https://youtube.com/watch?v=VIDEO_ID'); // ✅
convertYoutubeUrlToEmbed('https://youtu.be/VIDEO_ID'); // ✅
convertYoutubeUrlToEmbed('https://youtube.com/embed/VIDEO_ID'); // ✅
convertYoutubeUrlToEmbed('https://youtube.com/shorts/VIDEO_ID'); // ✅ NEW
📚 References
- Prisma Transactions
- Prisma Soft Delete Pattern
- URL.createObjectURL() Memory Management
- Mantine Skeleton Documentation
- React Toastify Documentation
📈 Comparison dengan QC Sebelumnya
| Aspek | Profil Desa | Potensi Desa | Berita Desa | Pengumuman | Gallery |
|---|---|---|---|---|---|
| Schema | 6/10 | 7/10 | 8/10 | 7/10 | 6/10 |
| API Security | 4/10 | 6/10 | 6/10 | 6/10 | 4/10 |
| API Design | 7/10 | 8/10 | 7.5/10 | 7/10 | 6/10 |
| UI/UX | 8/10 | 8.5/10 | 8/10 | 7.5/10 | 7.5/10 |
| State Mgmt | 7/10 | 8/10 | 8/10 | 7/10 | 6.5/10 |
| Code Quality | 7/10 | 7.5/10 | 7/10 | 6.5/10 | 6/10 |
| Overall | 6.5/10 | 7.5/10 | 7/10 | 6.5/10 | 6/10 |
Gallery memiliki score terendah karena:
- ❌ Critical bug
deletedAt @default(now())(sama seperti Profil & Pengumuman) - ❌ File orphaning issue (data integrity)
- ❌ Old file dihapus sebelum update confirmed (data loss risk)
- ❌ Tidak ada authentication di API
- ❌ Dead code (youtubeEmbed.tsx tidak digunakan)
- ❌ Memory leak potential
- ❌ Duplicate code (convertToEmbedUrl)
Tapi Gallery punya UI/UX yang bagus (7.5/10) dengan:
- ✅ Upload gambar dengan dropzone & preview
- ✅ YouTube embed conversion
- ✅ Rich text editor
- ✅ Responsive design
- ✅ Comprehensive validation
Dibuat oleh: QC Automation
Review Status: ⏳ Menunggu Review Developer
Next Review: Setelah implementasi fixes