Files
website-portofolio/issue.md

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

  1. Update prisma/schema.prisma → jalankan migrasi
  2. Update project-schema.ts
  3. Update actions.ts
  4. Update project-form.tsx
  5. Buat project-detail-modal.tsx
  6. Update projects-section.tsx
  7. Update file i18n