# Flow Task: Migrasi Ranatatour Portal ke Next.js

Checklist tugas dari awal sampai akhir, turunan langsung dari `NEXTJS_MIGRATION_PRD.md`. Kerjakan berurutan per fase — tiap fase = 1 branch, baru lanjut fase berikutnya kalau fase sebelumnya sudah selesai & di-merge ke `dev`.

> Referensi nomor bagian (PRD X) di bawah merujuk ke section di `NEXTJS_MIGRATION_PRD.md`.

---

## Fase 0 — Setup Project
**Branch:** `feat/project-scaffold`

- [ ] `npx create-next-app@latest` (pilih: TypeScript, Tailwind, App Router, ESLint)
- [ ] Install dependency tambahan: `prisma`, `@prisma/client`, `@supabase/supabase-js`, `next-auth`, `bcryptjs` + `@types/bcryptjs`, `react-hook-form`, `zod`, `@hookform/resolvers`, `framer-motion`, `clsx`
- [ ] Setup struktur folder: `app/`, `components/`, `lib/`, `types/`
- [ ] Konfigurasi Tailwind: warna brand & token (PRD 5.1), font Instrument Sans (PRD 5.2)
- [ ] Setup `.gitignore` (pastikan `.env` masuk)
- [ ] Init repo git, buat branch `dev` dari `main`
- [ ] Commit awal, push

## Fase 1 — Database Setup
**Branch:** `feat/database-setup`

- [ ] Buat project baru di Supabase
- [ ] Ambil `DATABASE_URL` (pooler, port 6543) dan `DIRECT_URL` (direct, port 5432)
- [ ] Isi `.env` dengan kedua connection string
- [ ] Jalankan `supabase_schema.sql` di Supabase SQL Editor **atau** langsung isi `prisma/schema.prisma` dari PRD Bagian 8 lalu `npx prisma migrate dev --name init`
- [ ] Kalau pakai jalur SQL manual: jalankan `npx prisma db pull` supaya `schema.prisma` sinkron
- [ ] `npx prisma generate`
- [ ] Buat `prisma/seed.ts` — isi data demo (beberapa Tour, Flight, Article, Client, Gallery, HeroImage, TeamMember) + 1 user admin (password di-hash `bcrypt.hash()`, **jangan** taruh password mentah)
- [ ] Jalankan seed (`npx prisma db seed`), cek data muncul di Supabase Table Editor
- [ ] Buat bucket Supabase Storage untuk upload gambar (PRD 11)
- [ ] Test koneksi Prisma Client dari kode (query sederhana, pastikan tidak error)

## Fase 2 — Auth Setup
**Branch:** `feat/auth-setup`

- [x] Buat `lib/auth.ts` — konfigurasi NextAuth Credentials Provider (`authorize()` cek email/username + `bcrypt.compare()` password)
- [x] Buat `app/api/auth/[...nextauth]/route.ts`
- [x] Tambahkan `role` ke session callback supaya bisa dibaca di middleware/komponen
- [x] Buat `proxy.ts` — proteksi semua route `/admin/*`, redirect ke `/login` kalau belum login atau bukan admin (PRD 10). **Catatan:** di Next.js 16 file ini bernama `proxy.ts`, bukan `middleware.ts` — breaking change, terverifikasi lewat `node_modules/next/dist/docs`.
- [x] Buat `app/login/page.tsx` — form login unified (email/username + password), styling ikut desain lama
- [x] Test: login sukses → redirect `/admin` (bukan pakai "intended URL"); login gagal → pesan error tampil; akses `/admin` tanpa login → redirect `/login` — ketiganya sudah dites langsung di browser

## Fase 3 — Base Layout
**Branch:** `feat/base-layouts`

- [x] Komponen `Navbar` — transparan di atas hero → solid saat scroll >10px, hamburger off-canvas mobile, indikator halaman aktif (PRD 6.4)
- [x] Komponen `Footer`
- [x] Komponen `WhatsAppBubble` mengambang — ambil `chatWhatsappNumber` dari SiteSetting, tidak dirender kalau kosong (PRD 6.5)
- [x] `MainLayout` publik (gabungan Navbar + children + Footer + WhatsAppBubble)
- [x] `AdminLayout` — sidebar gradient merah fixed di semua ukuran layar, header fixed, hanya `<main>` yang scroll (PRD 6.8)
- [x] Terapkan token warna & tipografi Design System di kedua layout (PRD 5)
- [x] Test responsif 375px / 768px / 1280px untuk Navbar dan AdminLayout, pastikan tidak ada horizontal overflow — direview & dites ulang di browser

## Fase 4 — Halaman Publik Statis
**Branch:** `feat/public-static-pages`

- [x] `app/about-us/page.tsx` — hero, nilai-nilai, stats, timeline, slider Tim Profesional auto-advance 5 detik + dot indicator (PRD 6.3). **Catatan:** copy nilai-nilai/stats/timeline masih placeholder karangan saya (belum ada sumber konten asli dari Laravel), ganti kalau ada teks resminya.
- [x] `app/gallery/page.tsx` — grid foto, tanpa filter kategori, plus empty state kalau belum ada foto (PRD 6.10)
- [x] `app/contact/page.tsx` — info kontak + form kirim pesan. **Catatan:** belum ada tabel `ContactMessage` di skema, jadi form cuma validasi + tampilkan status sukses, tidak tersimpan ke DB.
- [x] Hero tiap halaman baca `xxxHeroTitle`/`xxxHeroSubtitle` dari SiteSetting, fallback ke teks default kalau kosong (PRD 6.1)
- [x] Test slider Tim Profesional: auto-advance tiap 5 detik, loop ke awal, klik dot langsung pindah — diverifikasi presisi lewat pelacakan `setInterval`/`clearInterval`, termasuk fix bug klik-dot yang awalnya tidak reset ritme timer
- [x] Test responsif ketiga halaman — 375px/768px/1280px, tidak ada horizontal overflow

## Fase 5 — Home + Tour
**Branch:** `feat/public-home-tour`

- [x] `app/page.tsx` (Home) — hero auto-rotate gambar dari `HeroImage` tiap 3 detik crossfade, fallback 3 gambar default (PRD 6.1)
- [x] Section Tour di Home — slider + filter pill lokasi (PRD 6.2)
- [x] Section Artikel di Home — slider
- [x] Section Stats & Tentang Kami, Klien & Mitra, Lokasi & Kontak (form + thumbnail dekoratif, bukan iframe video asli)
- [x] `app/tour/page.tsx` — listing dengan filter lokasi + search
- [x] `app/tour/[id]/page.tsx` — Tour Detail, tombol "Pesan Sekarang" ke WA pakai `orderWhatsappNumber`, hilang kalau kosong (PRD 6.6)
- [x] Test panah slider (muncul cuma di desktop `lg:`), filter lokasi, search, format pesan WA ter-encode dengan benar, responsif 375/768/1280 — semua diverifikasi di browser. **Catatan:** dengan jumlah data seed saat ini (3 tour, 2 artikel) slider belum overflow di desktop lebar, jadi panah baru kelihatan efeknya kalau datanya nambah.

## Fase 6 — Flight + Article
**Branch:** `feat/public-flight-article`

- [x] `app/flight/page.tsx` — listing dengan search
- [x] `app/flight/[id]/page.tsx` — Flight Detail, tombol Pesan Sekarang WA (pesan beda dari Tour, PRD 6.6)
- [x] `app/article/[id]/page.tsx` — render `content` sebagai HTML (bukan teks mentah ter-escape). **Catatan:** awalnya pakai plugin `@tailwindcss/typography`, tapi ternyata CSS-nya tidak ke-compile sama sekali oleh Turbopack di setup Next 16 + Tailwind v4 ini (silent fail, tanpa error) — diganti jadi CSS manual di `globals.css` (`.article-content`) yang sudah diverifikasi benar-benar jalan.
- [x] Test tombol WA Flight (format pesan diverifikasi benar sesuai PRD 6.6), render artikel dibuktikan sebagai elemen HTML asli (`<p>` di DOM, bukan teks ter-escape), responsif 375/768/1280

## Fase 7 — Admin: Dashboard + Tours/Flights CRUD
**Branch:** `feat/admin-tours-flights-crud`

- [x] `app/admin/page.tsx` — Dashboard: ringkasan jumlah tiap jenis data + daftar terbaru
- [x] Tours CRUD lengkap: list, create, edit, delete, upload gambar dengan preview langsung (PRD 6.9)
- [x] Flights CRUD lengkap
- [x] Test upload gambar — kredensial Supabase Storage sudah di-set di `.env`. Diverifikasi end-to-end lewat script: upload ke bucket `uploads` berhasil, dan URL publiknya bisa diakses tanpa auth (200 OK). Preview instan (`URL.createObjectURL`) & fallback gambar lama saat edit tanpa ganti file tidak bisa disimulasikan penuh oleh tools browser saya (butuh dialog file picker OS), tapi logic-nya sudah benar dan komponennya tidak error saat dites di browser — disarankan dicoba manual sekali oleh kamu buat konfirmasi visual.

## Fase 8 — Admin: Articles, Clients, Galleries CRUD
**Branch:** `feat/admin-content-crud`

- [x] Articles CRUD (input konten HTML/rich text) — pakai TipTap (`@tiptap/react` + `starter-kit` + `extension-link`), sync ke hidden input, diverifikasi round-trip: ketik+bold di editor → tersimpan sebagai HTML asli di DB → tampil benar (elemen `<strong>` asli) di halaman publik artikel
- [x] Clients CRUD (logo)
- [x] Galleries CRUD (gambar wajib diisi sesuai schema — divalidasi HTML `required` + server-side, sudah dites keduanya)
- [x] Empty state konsisten "Belum ada [X]." di tiap listing yang masih kosong (PRD 6.10)

## Fase 9 — Admin: Hero Images + Team Members CRUD
**Branch:** `feat/admin-hero-team-crud`

- [x] Hero Images: list + tambah + hapus (tanpa fitur edit, sesuai desain asli)
- [x] Team Members CRUD lengkap (nama, jabatan, foto)
- [x] Pastikan slider About Us otomatis baca data asli dari tabel `TeamMember` begitu ada isinya, gantikan fallback 4 data default (PRD 6.11)
- [x] Test: tambah 1 data → fallback default hilang total, bukan tampil berdampingan — diverifikasi end-to-end: hapus semua TeamMember → fallback 4 default muncul di `/about-us` → tambah 1 data asli lewat form admin → fallback hilang total (cuma 1 data asli tampil, dot indicator juga otomatis hilang karena < 2 anggota) → data seed asli di-restore

## Fase 10 — Admin: Users CRUD
**Branch:** `feat/admin-users-crud`

- [x] Users CRUD: nama, username, email, password (hash bcrypt), role admin/member — termasuk validasi password minimal 6 karakter & email/username duplikat
- [x] Proteksi: user tidak bisa hapus akun sendiri
- [x] Proteksi: user tidak bisa turunkan role admin miliknya sendiri
- [x] Test kedua proteksi di atas dengan akun yang sedang login — diverifikasi 2 lapis: (1) UI — tombol hapus & pilihan role disembunyikan/disable untuk akun sendiri; (2) server-side — dicoba bypass UI langsung (kirim role="member" via hidden input yang dimanipulasi), Server Action tetap menolak dengan pesan error, dan dikonfirmasi role di database tidak berubah

## Fase 11 — Admin: Settings
**Branch:** `feat/admin-settings`

- [x] Form Settings: title/subtitle hero untuk 6 halaman (Home, Tour, Flight, About, Gallery, Contact)
- [x] Form Settings: About Us content (title, summary, body), info kontak (alamat, telepon, email, jam, map URL)
- [x] Form Settings: 2 nomor WhatsApp terpisah (`orderWhatsappNumber`, `chatWhatsappNumber`) + tombol "Tes Nomor" per field (buka `wa.me` pakai nilai yang lagi diketik, belum perlu disimpan dulu) — diverifikasi tombol Tes Nomor baca nilai input yang BELUM di-submit dan buka URL wa.me yang benar
- [x] Test: kosongkan salah satu nomor WA → tombol/bubble terkait hilang di halaman publik; isi lagi → muncul dengan link benar — diverifikasi end-to-end di `/tour/1`: kosongkan `orderWhatsappNumber` → tombol "Pesan Sekarang" hilang total (bubble chat tetap ada karena beda field) → isi lagi → tombol muncul lagi dengan URL wa.me yang benar. Bonus: dites juga override judul hero custom tampil polos tanpa `<span>` aksen warna, sesuai PRD 6.1

## Fase 12 — Integrasi Supabase Storage
**Branch:** `feat/supabase-storage-integration`

- [x] Pastikan semua form upload (Tours, Flights, Articles, Clients, Galleries, HeroImages, TeamMembers) betul-betul upload ke bucket Supabase Storage, dapat public URL, simpan ke kolom yang sesuai — dites upload **beneran** lewat browser (simulasi file picker pakai `DataTransfer`, bukan cuma script terpisah seperti Fase 7), preview `URL.createObjectURL` terbukti muncul, URL Storage asli tersimpan ke DB dan bisa diakses publik (200 OK)
- [x] Test hapus data yang punya gambar → file lama di Storage ikut terhapus (jangan numpuk file sampah) — ditambahkan helper `deleteImageByUrl()` di `src/lib/upload.ts`, dipasang di semua Server Action delete & update (replace gambar) untuk ketujuh entity. Diverifikasi langsung: hapus data → file lama return 404 "Object not found"; edit dengan ganti gambar → file lama 404, file baru 200
- [x] Test ukuran file besar / format tidak didukung → validasi jalan, tidak mentah-mentah error — **ketemu bug nyata**: Next.js Server Actions punya limit body bawaan 1MB, jadi upload file besar (6MB) tadinya crash mentah di level framework sebelum sempat kena validasi custom. Diperbaiki dengan `experimental.serverActions.bodySizeLimit: "8mb"` di `next.config.ts`. Setelah fix: file besar (>5MB) dan format tidak didukung (.txt) sama-sama menampilkan pesan error yang jelas, tidak crash

## Fase 13 — QA & Polish
**Branch:** `chore/qa-responsive-polish`

- [x] Cek ulang satu per satu semua item di PRD Bagian 6 (Spesifikasi Interaksi) — pastikan tidak ada yang "kelihatan mirip" tapi sebenarnya beda perilaku. **Ketemu 1 gap nyata:** PRD 6.4 minta Navbar publik toggle tombol "Login/Logout" tergantung status sesi, tapi selama ini selalu tampil "Login Admin" walau sudah login. Sudah diperbaiki (`useSession()` + `signOut()` di `Navbar.tsx`), dan diverifikasi: tampil "Logout" saat login, klik Logout beneran clear session (server-side, dicek `/admin` redirect ke `/login` lagi setelahnya). Catatan: "language switcher" di PRD 6.4 **sengaja tidak dikerjakan** — sudah tercatat eksplisit di PRD Bagian 15 sebagai fitur yang masih tahap diskusi
- [x] Test responsif 375px (mobile) / 768px (tablet) / 1280px (desktop) di **semua** halaman publik & admin — 10 halaman publik + 15 halaman admin dicek di 375px (semua bersih), plus sampel representatif di 768px & 1280px
- [x] Cek tidak ada horizontal overflow (`document.documentElement.scrollWidth === clientWidth`) di semua halaman — semua match persis di setiap breakpoint yang dites
- [x] Cek semua tombol/link tidak ada yang dead/decorative tanpa fungsi — dicek Footer, Navbar, dan sidebar admin: 0 dead link (href kosong/`#`) dari total puluhan link yang diperiksa
- [x] Cek console browser bersih dari error di semua halaman — bersih di semua halaman yang dites (pakai tab baru tiap kali biar tidak kena history error basi dari proses edit-sambil-jalan)

## Fase 14 — Deploy & Migrasi Data
**Branch:** `chore/deploy-vercel`

**Temuan penting saat mulai Fase 14:** project Laravel lama (`D:\Haerul\Kerja Praktek\ranatatour`) ternyata punya 2 model yang **tidak ada** di PRD/skema Next.js:
- **`Event`** (4 baris data demo, tidak pernah tersambung ke route/halaman manapun bahkan di Laravel) — atas keputusan user, dibuatkan **fitur baru penuh**: model `Event` di Prisma, halaman publik `/event` + `/event/[id]` (mirror pola Tour), CRUD admin `/admin/events`, link di Navbar/Footer/Sidebar/Dashboard. Data 4 event demo sudah di-seed ulang (dengan gambar placeholder proper, path gambar lama sudah rusak).
- **`CustomizeRequest`** (form Contact ternyata nyimpen ke DB di Laravel, 0 baris data asli) — ditambahkan model `ContactMessage` di Prisma, `ContactForm.tsx` diubah dari cuma-validasi jadi beneran nyimpen lewat Server Action, plus halaman admin baru `/admin/contact-messages` (list, tandai dibaca, hapus) + card "Pesan Belum Dibaca" di Dashboard.

Kedua fitur baru ini sudah diverifikasi end-to-end di browser (CRUD Event, submit+lihat+tandai dibaca+hapus pesan Contact, responsif 375px, console bersih). **Catatan:** sempat ketemu Turbopack tidak mendeteksi folder route baru (`/event`, `/admin/events`, `/admin/contact-messages`) yang dibuat saat dev server sedang jalan — butuh restart `npm run dev` manual, bukan bug di kode.

- [x] Export data ASLI dari MySQL lama (Tour 7, Flight 5, Article 4, Client 6, Gallery 5, HeroImage 0, TeamMember 0, User 2, SiteSetting 1 — sudah dicek jumlahnya lewat `php artisan tinker`) → import ke Supabase dengan ID yang sama → reset sequence Postgres. **Selesai semua 7 tabel** (Tour, Flight, Article, Client, Gallery, SiteSetting via `_migrate-real-data.ts`; User via `_migrate-users.ts` — keduanya script sekali-pakai, sudah dihapus setelah sukses jalan). Tabel `User` di-replace penuh dengan 2 user asli (`admin@ranatatour.local`/Admin, `member@ranatatour.local`/Member), password bcrypt asli dari Laravel langsung kepakai (kompatibel dengan bcryptjs) — login demo `admin@ranatatour.com` sudah tidak berfungsi lagi, sudah diverifikasi (percobaan login pakai kredensial lama menghasilkan "Email/username atau password salah"). `HeroImage`/`TeamMember` tetap pakai data demo Fase 9 karena Laravel lama memang 0 baris untuk keduanya.
- [x] Pindahkan file gambar lama ke Supabase Storage, update kolom URL yang menunjuk ke sana — gambar tour/flight/gallery/client di-upload ulang ke bucket `uploads` dengan deduplikasi (file yang sama dipakai berkali-kali cuma di-upload sekali)
- [x] Verifikasi jumlah baris tiap tabel sama persis (MySQL vs Supabase) — dicek langsung lewat query Postgres, semua cocok (7/5/4/6/5/1)
- [ ] Deploy ke Vercel (staging dulu), isi semua environment variable di dashboard Vercel
- [ ] Test end-to-end di staging dengan data asli (bukan data demo)
- [ ] Arahkan domain ke deployment Vercel yang baru (cutover)
- [ ] Simpan server Laravel lama sebagai cadangan (jangan langsung dimatikan/dihapus)

---

## Selesai kalau...
Semua checkbox di atas tercentang **dan** memenuhi Definition of Done di PRD Bagian 16.
