10 KiB
Issue: Peningkatan Bagian Proyek (Project Section Enhancement)
Deskripsi
Setiap kartu proyek perlu diperkaya dengan informasi tambahan: tech stack, keterangan lengkap, interaksi klik untuk detail, tahun pengerjaan, dan target/pencapaian yang berhasil tercapai.
Langkah-langkah Implementasi
Langkah 1 — Update Skema Prisma (Database)
File: prisma/schema.prisma
Tambahkan kolom baru pada model Project:
model Project {
// ... field yang sudah ada ...
techStack String[] // array nama teknologi, e.g. ["Java", "Spring Boot", "PostgreSQL"]
year Int? // tahun pengerjaan, e.g. 2024
highlights String[] // target/pencapaian, e.g. ["Reduce latency 40%", "10K daily users"]
}
Setelah edit schema, jalankan migrasi:
npx prisma migrate dev --name add_project_details
npx prisma generate
Langkah 2 — Update Zod Schema Validasi
File: src/features/projects/project-schema.ts
Tambahkan field baru ke projectSchema:
techStack: z.string().optional(), // dikirim sebagai string CSV dari form, e.g. "Java,Spring Boot,Docker"
year: z.coerce.number().optional(),
highlights: z.string().optional(), // dikirim sebagai string CSV, e.g. "Reduce latency 40%,10K users"
Catatan: Field diproses sebagai CSV string di form, lalu di-parse menjadi
String[]sebelum disimpan ke DB.
Langkah 3 — Update Server Action (Create & Update)
File: src/features/projects/actions.ts
Di fungsi createProjectAction dan updateProjectAction, tambahkan parsing:
const rawTechStack = formData.get("techStack") as string;
const rawHighlights = formData.get("highlights") as string;
// Di blok prisma.project.create / prisma.project.update:
techStack: rawTechStack ? rawTechStack.split(",").map(s => s.trim()) : [],
year: formData.get("year") ? Number(formData.get("year")) : null,
highlights: rawHighlights ? rawHighlights.split(",").map(s => s.trim()) : [],
Langkah 4 — Update Form Admin (Tambah Field Baru)
File: src/features/projects/project-form.tsx
Tambahkan tiga input baru di dalam <form>:
4a. Tech Stack (input teks, pisahkan dengan koma)
<div className="space-y-2">
<label className="text-sm font-semibold">Tech Stack</label>
<input
name="techStack"
defaultValue={initialData?.techStack?.join(", ") || ""}
placeholder="Java, Spring Boot, PostgreSQL, Docker"
className="..."
/>
<p className="text-xs text-muted-foreground">Pisahkan setiap teknologi dengan koma</p>
</div>
4b. Tahun Pengerjaan (input number)
<div className="space-y-2">
<label className="text-sm font-semibold">Tahun Pengerjaan</label>
<input
name="year"
type="number"
min="2000"
max="2099"
defaultValue={initialData?.year || new Date().getFullYear()}
placeholder="2024"
className="..."
/>
</div>
4c. Pencapaian / Highlights (input teks, pisahkan dengan koma)
<div className="space-y-2">
<label className="text-sm font-semibold">Target & Pencapaian</label>
<textarea
name="highlights"
defaultValue={initialData?.highlights?.join(", ") || ""}
rows={3}
placeholder="Reduce API latency 40%, Handle 10K daily users, 99.9% uptime"
className="..."
/>
<p className="text-xs text-muted-foreground">Pisahkan setiap pencapaian dengan koma</p>
</div>
Langkah 5 — Update Tampilan Kartu Proyek (Project Card)
File: src/features/projects/projects-section.tsx
5a. Tampilkan Tech Stack sebagai badge di kartu
Di dalam project-card, ganti bagian tags yang lama dengan tech stack:
{/* Tech Stack Badges */}
{project.techStack?.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-auto">
{project.techStack.map((tech: string) => (
<span key={tech} className="px-2 py-1 rounded-md text-[10px] font-mono bg-accent/10 text-accent border border-accent/20">
{tech}
</span>
))}
</div>
)}
5b. Tampilkan Tahun di pojok kanan atas
{project.year && (
<span className="text-[10px] font-mono text-muted-foreground">
{project.year}
</span>
)}
Langkah 6 — Buat Komponen Detail Modal / Drawer
Buat file baru: src/features/projects/project-detail-modal.tsx
Komponen modal yang muncul ketika kartu proyek di-klik. Isi detail:
- Judul proyek
- Deskripsi lengkap
- Tahun pengerjaan
- Tech stack (badge berwarna)
- Highlights / pencapaian sebagai checklist ✅
- Tombol link ke repo dan live demo
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, CheckCircle2, ExternalLink, GitFork, Calendar } from "lucide-react";
export function ProjectDetailModal({ project, onClose }: { project: any; onClose: () => void }) {
return (
<AnimatePresence>
{/* Backdrop */}
<motion.div
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
>
{/* Modal container */}
<motion.div
className="relative w-full max-w-2xl bg-card border border-border rounded-2xl p-8 shadow-xl overflow-y-auto max-h-[85vh]"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button onClick={onClose} className="absolute top-4 right-4 p-2 rounded-full hover:bg-muted text-muted-foreground">
<X size={18} />
</button>
{/* Header */}
<h2 className="text-2xl font-bold mb-1">{project.title}</h2>
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-4">
<Calendar size={14} />
<span>{project.year}</span>
</div>
{/* Cover image */}
{project.imageUrl && (
<img src={project.imageUrl} alt={project.title} className="w-full h-48 object-cover rounded-xl mb-6" />
)}
{/* Description */}
<p className="text-sm leading-relaxed text-muted-foreground mb-6">{project.description}</p>
{/* Tech Stack */}
{project.techStack?.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-semibold mb-2">Tech Stack</h4>
<div className="flex flex-wrap gap-2">
{project.techStack.map((tech: string) => (
<span key={tech} className="px-3 py-1.5 rounded-lg text-xs font-mono bg-accent/10 text-accent border border-accent/20">
{tech}
</span>
))}
</div>
</div>
)}
{/* Highlights */}
{project.highlights?.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-semibold mb-2">Target & Pencapaian</h4>
<ul className="space-y-2">
{project.highlights.map((item: string, i: number) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<CheckCircle2 size={16} className="text-success mt-0.5 shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</div>
)}
{/* Links */}
<div className="flex gap-3 pt-4 border-t border-border/50">
{project.repoUrl && (
<a href={project.repoUrl} target="_blank" rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-muted hover:bg-muted/80 text-sm font-medium transition-colors">
<GitFork size={14} /> Repository
</a>
)}
{project.liveUrl && (
<a href={project.liveUrl} target="_blank" rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-accent text-accent-foreground text-sm font-medium hover:opacity-90 transition-opacity">
<ExternalLink size={14} /> Live Demo
</a>
)}
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}
Langkah 7 — Integrasikan Modal ke Kartu Proyek
File: src/features/projects/projects-section.tsx
Tambahkan state untuk kontrol modal:
const [selectedProject, setSelectedProject] = useState<any | null>(null);
Bungkus project-card dengan onClick:
<div
className="project-card group ... cursor-pointer"
onClick={() => setSelectedProject(project)}
>
{/* ... isi kartu ... */}
</div>
Render modal di luar grid:
{selectedProject && (
<ProjectDetailModal
project={selectedProject}
onClose={() => setSelectedProject(null)}
/>
)}
Langkah 8 — Update i18n (Opsional)
File: messages/id.json dan messages/en.json
Tambahkan key terjemahan baru jika ada label yang perlu diterjemahkan, misalnya:
"Projects": {
"techStack": "Tech Stack",
"year": "Tahun",
"highlights": "Pencapaian"
}
Ringkasan Perubahan File
| File | Aksi | Keterangan |
|---|---|---|
prisma/schema.prisma |
MODIFY | Tambah kolom techStack, year, highlights |
src/features/projects/project-schema.ts |
MODIFY | Tambah validasi field baru |
src/features/projects/actions.ts |
MODIFY | Parse & simpan field baru ke DB |
src/features/projects/project-form.tsx |
MODIFY | Tambah input TechStack, Year, Highlights |
src/features/projects/projects-section.tsx |
MODIFY | Tampilkan data baru, integrasikan modal |
src/features/projects/project-detail-modal.tsx |
NEW | Komponen modal detail proyek |
messages/id.json |
MODIFY | Tambah key terjemahan baru |
messages/en.json |
MODIFY | Tambah key terjemahan baru |
Urutan Pengerjaan yang Disarankan
- ✅ Update
prisma/schema.prisma→ jalankan migrasi - ✅ Update
project-schema.ts - ✅ Update
actions.ts - ✅ Update
project-form.tsx - ✅ Buat
project-detail-modal.tsx - ✅ Update
projects-section.tsx - ✅ Update file i18n