# PRD: Migrasi Ranatatour Portal ke Next.js

## 1. Ringkasan

Dokumen ini adalah rencana migrasi website **Ranatatour Portal** dari stack lama (Laravel 13 + Inertia.js + React + MySQL, hosting PHP tradisional) ke stack baru berbasis **Next.js**, supaya bisa di-deploy native di **Vercel** tanpa workaround, dan tidak lagi bergantung pada hosting PHP.

Proyek ini adalah **rebuild total di repo baru**, bukan migrasi bertahap di repo yang sama. Semua fitur, tampilan, dan perilaku interaktif yang sudah ada di project Laravel harus direplikasi (feature & UX parity) — migrasi ini tidak menambah fitur baru, hanya memindahkan teknologi.

## 2. Alasan Migrasi

- Laravel tidak punya runtime resmi di Vercel (harus pakai runtime PHP tidak resmi, dengan banyak batasan: storage file lokal hilang, session file-based tidak cocok serverless, tidak ada cron bawaan).
- Next.js adalah framework native Vercel — deploy tinggal push, tanpa konfigurasi tambahan.
- Satu bahasa (TypeScript/JavaScript) untuk frontend & backend, tidak perlu context-switch PHP ↔ JS.
- Ekosistem modern (Prisma, Supabase, NextAuth) yang saling terintegrasi dengan baik untuk kebutuhan proyek ini.

## 3. Tech Stack Baru

| Kebutuhan | Stack Lama (Laravel) | Stack Baru (Next.js) |
|---|---|---|
| Framework | Laravel 13 + Inertia.js v3 | Next.js 15 (App Router) |
| Bahasa | PHP + JS (React) | TypeScript (full-stack) |
| UI | React 19 + Tailwind v4 | React 19 + Tailwind v4 (tetap, palet warna & desain dipertahankan) |
| Database | MySQL | PostgreSQL (via Supabase) |
| ORM | Eloquent | Prisma |
| Auth | Custom (session, role di tabel `users`) | Auth.js (NextAuth) — Credentials Provider, role di tabel `User` |
| Upload file | Local disk (`storage/app/public` + symlink) | Supabase Storage (S3-compatible) |
| Validasi form | Laravel Request validation | Zod + React Hook Form |
| Hosting | VPS/shared hosting PHP | Vercel |
| Database hosting | MySQL lokal/VPS | Supabase (Postgres terkelola) |

## 4. Cakupan Fitur (Feature Parity Checklist)

### 4.1 Halaman Publik
- [ ] Home — hero slider (gambar dari admin, fallback default), section Tour (slider + filter lokasi), section Artikel (slider), Stats & Tentang Kami, Klien & Mitra, Lokasi & Kontak (form + thumbnail dekoratif)
- [ ] Tour — listing dengan filter lokasi + search
- [ ] Tour Detail — deskripsi lengkap + tombol "Pesan Sekarang" ke WhatsApp
- [ ] Flight — listing dengan search
- [ ] Flight Detail — info rute + tombol "Pesan Sekarang" ke WhatsApp
- [ ] Article Detail — konten artikel (render HTML, bukan teks mentah)
- [ ] About Us — hero, nilai-nilai, stats, timeline, tim profesional (slider auto 5 detik + dot indicator)
- [ ] Gallery — grid foto (tanpa filter kategori, sesuai keputusan terakhir)
- [ ] Contact — info kontak + form kirim pesan
- [ ] Navbar — hamburger menu mobile, indikator halaman aktif, transparan di atas hero lalu solid saat scroll
- [ ] Footer — link ke semua halaman
- [ ] Floating WhatsApp chat bubble (nomor dari Settings, sembunyi kalau kosong)

### 4.2 Autentikasi
- [ ] Login unified (email/username + password) untuk admin
- [ ] Redirect otomatis ke `/admin` setelah login sukses berdasarkan role
- [ ] Middleware proteksi semua route `/admin/*`

### 4.3 Admin Panel
- [ ] Dashboard — ringkasan jumlah data + daftar terbaru
- [ ] Tours CRUD (dengan upload gambar)
- [ ] Flights CRUD (dengan upload gambar)
- [ ] Articles CRUD (dengan upload gambar)
- [ ] Clients CRUD (logo perusahaan)
- [ ] Galleries CRUD (dengan upload gambar)
- [ ] Hero Images CRUD (khusus tambah/hapus, untuk slider Home)
- [ ] Team Members CRUD (nama, jabatan, foto, untuk About Us)
- [ ] Users CRUD (nama, username, email, password, role admin/member — dengan proteksi tidak bisa hapus/turunkan diri sendiri)
- [ ] Settings — judul/subjudul hero tiap halaman, konten About Us, info kontak, 2 nomor WhatsApp terpisah (pesanan & chat bubble) dengan fitur "Tes Nomor"
- [ ] Layout admin: sidebar & navbar fixed, hanya konten yang scroll

## 5. Design System & Panduan Visual

Bagian ini wajib dibaca sebelum mulai styling apa pun — supaya tampilan Next.js konsisten sama persis dengan versi Laravel, bukan cuma "mirip-mirip".

### 5.1 Warna

| Token | Nilai | Dipakai untuk |
|---|---|---|
| Brand merah (`merah-ranata`) | `#991612` | Tombol utama, aksen teks, badge, ikon |
| Hover tombol merah | `#7a1210` | **Wajib** dipakai untuk hover, bukan `red-700`/`red-800` Tailwind bawaan |
| Merah alternatif (section CTA tertentu) | `#A4161A` | Dipakai di beberapa section About Us — pertimbangkan disatukan ke `#991612` saat rebuild supaya tidak ada 2 shade merah yang beda tipis |
| Heading utama | `#4b2e2b` | Judul section, judul card |
| Teks sekunder | `#7c6f6a` | Subjudul, deskripsi, isi paragraf |
| Teks muted/caption | `#9b8f8a` | Harga sekunder, durasi, jabatan tim, label kecil |
| Background halaman | `#FAF7F2` | Background utama semua halaman publik |
| Background section alternatif | `#ffffff` / `#FAF7F2` | Section berselang-seling putih dan krem |

> **Kenapa hover harus `#7a1210`:** pernah ada bug nyata di versi Laravel — semua tombol solid merah pakai `hover:bg-red-800`/`hover:bg-red-700` Tailwind bawaan, padahal warna itu HAMPIR SAMA PERSIS dengan `#991612`, jadi hover-nya nyaris tidak terlihat sama sekali. Perbaikannya adalah pakai shade yang jelas lebih gelap. **Jangan ulangi kesalahan ini di Next.js** — selalu cek kontras warna hover sebelum dipakai.

### 5.2 Tipografi

- **Font:** Instrument Sans, di-expose lewat CSS variable (`--font-sans` di Tailwind config), harus benar-benar dipasang di elemen root (`<body className="font-sans antialiased">`). Pernah ada bug hilang gara-gara class `font-sans` tidak sengaja terhapus dari body, sehingga browser fallback ke font default — pastikan ini selalu ada dan dites dengan cek `getComputedStyle(document.body).fontFamily`.
- **Bobot heading:** semua judul section (H2) pakai `font-semibold`, **BUKAN** `font-bold` — ini keputusan desain yang disengaja supaya kesan lebih ringan/elegan, bukan tebal berat.
- **Ukuran heading section:** konsisten `text-3xl` di semua halaman (Home, Tour, Flight, About Us, Gallery, Contact) — jangan campur `text-2xl` di sebagian halaman.
- **Card:** judul card pakai `font-semibold` (bukan `font-bold`), ukuran `text-sm`/`text-base` tergantung ukuran card.
- **Hero title:** `text-4xl md:text-6xl font-semibold leading-tight`.
- **Hero subtitle:** `text-base md:text-lg`, warna `text-white/80`.
- **Kicker/label hero:** `text-sm uppercase tracking-[0.2em] text-white/80` (contoh: "Ranata Tour" di atas judul hero).

### 5.3 Spacing & Layout Dasar

- Container utama: `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` untuk konten halaman biasa.
- Section publik: padding vertikal `py-20` (section besar) atau `py-10` (section kecil, misal Artikel di Home).
- Card: border radius konsisten `rounded-2xl` untuk card besar, `rounded-full` untuk pill/badge/tombol.
- Shadow: `shadow-sm` untuk card diam, naik ke `shadow-md`/`shadow-xl` saat hover.

## 6. Spesifikasi Interaksi & Perilaku Komponen

Ini bagian paling penting yang hilang di draft PRD sebelumnya — detail **bagaimana tiap komponen berperilaku**, bukan cuma daftar fitur.

### 6.1 Hero Section (tiap halaman)

Struktur umum semua hero:

```
<section className="relative min-h-[X] overflow-hidden text-white px-6 md:px-10">
  {/* gambar background, object-cover, absolute inset-0 */}
  <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-black/20" />
  <div className="relative z-10 mx-auto flex min-h-[X] max-w-6xl flex-col justify-center px-6 pt-24 md:px-10">
    <p>Kicker label uppercase</p>
    <h1>Judul (bisa ada <span> warna aksen di kata tertentu)</h1>
    <p>Subjudul</p>
    {/* CTA tombol, khusus Home saja */}
  </div>
</section>
```

| Halaman | Tinggi hero (`min-h`) | Padding atas konten | Background |
|---|---|---|---|
| Home | `80vh` | `pt-28` | Slider gambar auto-rotate 3 detik, crossfade opacity, sumber dari tabel `HeroImage` (fallback ke gambar default kalau kosong) |
| Tour / Flight / Gallery / Contact | `60vh` | `pt-24` | 1 gambar statis |
| About Us | `50vh` | `pt-24` | 1 gambar statis |

- **Judul & subjudul hero bisa diedit admin** (lewat Settings, field `xxxHeroTitle`/`xxxHeroSubtitle` per halaman). Kalau field kosong, tampilkan teks default yang sudah di-hardcode di komponen (termasuk `<span>` warna aksen di kata tertentu). Kalau admin isi teks custom, tampilkan apa adanya sebagai teks polos (tanpa span warna, karena itu teks bebas dari database, bukan JSX).
- Overlay gradient **wajib** `bg-gradient-to-b from-black/60 via-black/40 to-black/20` di semua hero — konsisten, jangan campur kombinasi gradient lain per halaman.

### 6.2 Slider / Carousel (Home: Tour & Artikel)

- Kontainer: `flex flex-nowrap gap-4 overflow-x-auto scrollbar-hide` (native horizontal scroll, bukan library carousel pihak ketiga).
- Lebar card responsif (persentase, bukan fixed px):
  - Tour: `w-[70%] sm:w-[45%] md:w-[32%] lg:w-[calc(25%-12px)]`
  - Artikel: `w-[42%] sm:w-[32%] md:w-[25%] lg:w-[calc(25%-12px)]`
- Panah navigasi kiri-kanan: bulat, posisi absolute di tepi kiri/kanan slider, **hanya muncul di desktop** (`hidden ... lg:inline-flex`) — sengaja disembunyikan di mobile/tablet karena di sana user swipe langsung pakai jari, panah cuma akan menuh-menuhi layar kecil.
- Klik panah memanggil `scrollBy({ left: ±320, behavior: 'smooth' })` pada ref container.
- Filter Tour (pill button lokasi): filter di client-side terhadap data yang sudah di-fetch, bukan re-fetch ke server tiap ganti filter.

### 6.3 Slider Tim Profesional (About Us) — Auto-Advance

Ini beda dari slider Home (yang manual via panah) — Tim Profesional **auto-advance sendiri**:

- `setInterval` tiap **5000ms**, index card aktif bertambah 1 lalu modulo jumlah total anggota tim (`(prev + 1) % team.length`) — supaya loop kembali ke awal setelah kartu terakhir.
- Kalau anggota tim cuma 1 (atau data masih fallback default), interval tetap jalan tapi otomatis skip kalau `team.length < 2` (tidak ada gunanya auto-advance kalau cuma 1 kartu).
- Ada **dot indicator** di bawah slider, satu dot per anggota tim, dot yang aktif diberi warna merah dan lebih lebar (`w-5 bg-merah-ranata` vs `w-2 bg-gray-300`).
- Dot **bisa diklik** — klik dot manapun langsung pindah ke kartu itu (`setActiveIndex(idx)`), dan otomatis "reset" ritme auto-advance berikutnya karena state berubah.
- Perpindahan kartu pakai `scrollTo({ left, behavior: 'smooth' })`, bukan CSS transform, supaya native scroll snap tetap terasa halus di sentuhan manual (swipe) maupun otomatis.

### 6.4 Navbar

- **Transparan di atas hero** (`bg-transparent`, teks putih) → berubah **solid putih + shadow** begitu user scroll melewati `window.scrollY > 10`. Kalau halaman tidak punya hero transparan (properti `transparentOnTop=false`), navbar selalu solid dari awal.
- **Desktop (`lg:` ke atas):** logo kiri, menu navigasi di tengah (posisi absolute, centered), language switcher + tombol Login/Logout di kanan.
- **Mobile & tablet (di bawah `lg`):** menu tengah & kanan disembunyikan, muncul ikon hamburger di kanan. Klik hamburger membuka **sidebar off-canvas dari kanan** (`translateX` animasi 200ms), dengan overlay hitam transparan di belakangnya yang bisa diklik untuk menutup.
- **Indikator halaman aktif:**
  - Desktop: garis bawah merah tipis di link yang aktif (pseudo-element `after:`), teks jadi `font-semibold`.
  - Mobile (di sidebar off-canvas): background merah muda pudar (`bg-red-50`) + titik kecil merah di kanan link yang aktif.
- Deteksi halaman aktif berdasarkan pathname saat ini (`pathname === '/'` untuk Home persis, `pathname.startsWith(href)` untuk halaman lain).

### 6.5 Floating WhatsApp Chat Bubble

- Muncul fixed di kanan bawah semua halaman publik (lewat root layout, bukan per halaman).
- Nomor tujuan diambil dari Settings (`chatWhatsappNumber`), **bukan** dari nomor yang sama dengan tombol "Pesan Sekarang" — ini dua field terpisah yang sengaja dipisah supaya admin bisa arahkan ke nomor CS berbeda dari nomor sales/pemesanan.
- **Kalau nomor kosong, bubble tidak dirender sama sekali** (bukan disabled/abu-abu, benar-benar hilang dari DOM).
- Pesan pre-filled generik: "Halo Ranata Tour, saya ingin bertanya lebih lanjut."

### 6.6 Tombol "Pesan Sekarang" (Tour/Flight Detail)

- Nomor tujuan dari Settings (`orderWhatsappNumber`) — **satu nomor yang sama dipakai di semua Tour dan semua Flight**, bukan per-item.
- Kalau nomor kosong, tombol **tidak dirender sama sekali** di halaman detail.
- Pesan pre-filled beda per konteks:
  - Tour: `Halo Ranata Tour, saya tertarik dengan paket tour "{judul}" ({lokasi}). Mohon info lebih lanjut.`
  - Flight: `Halo Ranata Tour, saya tertarik memesan tiket pesawat {maskapai} ({nomor penerbangan}) rute {asal} - {tujuan}. Mohon info lebih lanjut.`
- Normalisasi nomor: hapus semua karakter non-digit, kalau diawali `0` ganti jadi `62` (format Indonesia ke format internasional WhatsApp), baru dibentuk jadi URL `https://wa.me/{nomor}?text={pesan ter-encode}`.

### 6.7 Kartu (Card) & Efek Hover

- Semua card tour/flight/artikel: gambar dengan `aspect-[4/3]`, saat hover kartu (`group-hover`) gambar **zoom halus** (`scale-105`, `transition duration-500`), dan shadow card naik dari `shadow-sm` ke `shadow-md`.
- Card Gallery: overlay gradasi gelap dari bawah (`bg-gradient-to-t from-black/80 via-black/20 to-transparent`) yang **fade-in saat hover** (`opacity-0 group-hover:opacity-100`), judul foto ikut translate masuk dari bawah (`translate-y-4 group-hover:translate-y-0`).
- Semua elemen interaktif (link, tombol, card) **wajib** ada class `transition` supaya perubahan state terasa halus, tidak instan/patah.

### 6.8 Admin Layout

- Sidebar (gradient `linear-gradient(160deg, #1c0a0a 0%, #6f0f0c 50%, #991612 100%)`) bersifat **fixed di semua ukuran layar**, bukan cuma di desktop — ini penting supaya sidebar & header admin tidak ikut ter-scroll waktu konten panjang (pernah jadi bug: sidebar dan navbar ikut scroll bareng konten karena salah pakai `position: static` di breakpoint desktop).
- Konten utama (`<main>`) diberi margin kiri (`lg:ml-64`) untuk kompensasi lebar sidebar yang fixed, dan **hanya `<main>` yang bisa di-scroll** (`overflow-y-auto`) — sidebar dan header admin diam di tempat.
- Mobile: sidebar jadi off-canvas juga (mirip pola Navbar publik), dibuka lewat hamburger di header admin.
- Setiap link menu sidebar yang match dengan halaman aktif diberi background putih + teks merah + titik indikator kecil di kanan.

### 6.9 Form Admin & Upload Gambar

- Semua input form: border abu-abu tipis, fokus jadi ring merah + border merah (`focus:ring-merah-ranata focus:border-merah-ranata`).
- Error validasi tampil di bawah input masing-masing, teks merah kecil (`text-red-500 text-xs`).
- Komponen upload gambar (`ImageUploadField`): begitu user pilih file, **langsung tampilkan preview** gambar itu (pakai `URL.createObjectURL`) sebelum form disubmit — user tidak perlu submit dulu baru tahu gambar yang dipilih benar.
- Saat mode edit, kalau user tidak pilih file baru, gambar lama tetap dipakai (tidak wajib upload ulang tiap edit).

### 6.10 Empty State

- Pola pesan konsisten di semua listing yang datanya masih kosong: **"Belum ada [nama data]."** dengan warna teks abu-abu/muted, kadang disertai ikon outline besar di atasnya (misal di Gallery) untuk kesan lebih ramah, bukan cuma teks polos.
- Contoh: "Belum ada gambar hero. Halaman Home akan memakai gambar bawaan." — pesan ini juga menjelaskan konsekuensi (bahwa sistem fallback ke default), bukan cuma bilang "kosong".

### 6.11 Fallback ke Data Default

Beberapa bagian sengaja didesain **tidak pernah tampil kosong** di halaman publik meski admin belum isi data — supaya website tidak pernah terlihat "rusak" waktu pertama kali di-deploy sebelum admin sempat mengisi konten:

| Data | Fallback kalau kosong |
|---|---|
| Gambar Hero (Home) | 3 URL gambar default (foto kota/travel) |
| Tim Profesional (About Us) | 4 anggota tim contoh (nama, jabatan, foto default) |
| Judul/subjudul hero per halaman | Teks default yang di-hardcode di komponen |

Begitu admin menambahkan **minimal 1 data asli**, seluruh data default itu **digantikan sepenuhnya** (bukan ditambahkan berdampingan dengan data default).

## 7. Responsive Behavior

- Breakpoint yang dipakai: `sm` (640px), `md` (768px), `lg` (1024px) — standar Tailwind, tidak pakai breakpoint custom.
- Pendekatan mobile-first: style dasar untuk mobile, ditambah override progresif tiap breakpoint naik.
- Titik pindah mobile-nav ↔ desktop-nav (Navbar publik maupun Admin sidebar) selalu di breakpoint `lg`.
- **Wajib dites di 3 ukuran** sebelum dianggap selesai: 375px (mobile), 768px (tablet), 1280px (desktop) — cek tidak ada horizontal overflow (`document.documentElement.scrollWidth` harus sama dengan `clientWidth`), dan semua elemen interaktif tetap bisa dijangkau/diklik di tiap ukuran.

## 8. Skema Database (Prisma)

```prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  username  String?  @unique
  email     String   @unique
  password  String
  role      String   @default("member") // "admin" | "member"
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Tour {
  id          Int      @id @default(autoincrement())
  title       String
  location    String?
  price       String?
  duration    String?
  badge       String?
  imageUrl    String?
  excerpt     String?
  description String?  @db.Text
  isFeatured  Boolean  @default(false)
  sortOrder   Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

model Flight {
  id            Int      @id @default(autoincrement())
  airline       String
  flightNumber  String?
  origin        String
  destination   String
  departureInfo String?
  price         String?
  badge         String?
  imageUrl      String?
  excerpt       String?
  description   String?  @db.Text
  isFeatured    Boolean  @default(false)
  sortOrder     Int      @default(0)
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

model Article {
  id          Int       @id @default(autoincrement())
  title       String
  excerpt     String?
  content     String?   @db.Text
  imageUrl    String?
  publishedAt DateTime?
  label       String?
  isFeatured  Boolean   @default(false)
  sortOrder   Int       @default(0)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

model Client {
  id         Int      @id @default(autoincrement())
  name       String
  logoUrl    String?
  isFeatured Boolean  @default(false)
  sortOrder  Int      @default(0)
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

model Gallery {
  id         Int      @id @default(autoincrement())
  title      String?
  imageUrl   String
  category   String?
  isFeatured Boolean  @default(false)
  sortOrder  Int      @default(0)
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

model HeroImage {
  id        Int      @id @default(autoincrement())
  imageUrl  String
  sortOrder Int      @default(0)
  createdAt DateTime @default(now())
}

model TeamMember {
  id        Int      @id @default(autoincrement())
  name      String
  role      String?
  photoUrl  String?
  sortOrder Int      @default(0)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model SiteSetting {
  id                  Int     @id @default(autoincrement())
  aboutTitle          String?
  aboutSummary        String? @db.Text
  aboutBody           String? @db.Text
  contactAddress      String? @db.Text
  contactPhone        String?
  contactEmail        String?
  contactHours        String?
  contactMapUrl       String?
  orderWhatsappNumber String?
  chatWhatsappNumber  String?
  homeHeroTitle       String?
  homeHeroSubtitle    String? @db.Text
  tourHeroTitle       String?
  tourHeroSubtitle    String? @db.Text
  flightHeroTitle     String?
  flightHeroSubtitle  String? @db.Text
  aboutHeroTitle      String?
  aboutHeroSubtitle   String? @db.Text
  galleryHeroTitle    String?
  galleryHeroSubtitle String? @db.Text
  contactHeroTitle    String?
  contactHeroSubtitle String? @db.Text
}
```

> Catatan:
> - `SiteSetting` tetap didesain sebagai singleton (hanya 1 baris), sama seperti di Laravel.
> - ID sengaja dibuat `Int @id @default(autoincrement())` (bukan `cuid()`), supaya **sama persis dengan ID auto-increment di MySQL** — ini penting supaya data lama (tour, flight, artikel, dst) bisa dipindah ke Supabase tanpa perlu mapping ulang ID, dan URL seperti `/tour/1` tetap valid setelah migrasi.
> - `directUrl` dipakai khusus untuk migration (koneksi langsung port 5432), sementara `url` (lewat connection pooler port 6543) dipakai aplikasi saat runtime di Vercel.

## 9. Struktur Routing (App Router)

```
app/
  page.tsx                          → Home ("/")
  tour/page.tsx                     → Tour listing
  tour/[id]/page.tsx                → Tour detail
  flight/page.tsx                   → Flight listing
  flight/[id]/page.tsx              → Flight detail
  article/[id]/page.tsx             → Article detail
  about-us/page.tsx                 → About Us
  gallery/page.tsx                  → Gallery
  contact/page.tsx                  → Contact
  login/page.tsx                    → Login

  admin/
    layout.tsx                      → AdminLayout (guard + sidebar/navbar)
    page.tsx                        → Dashboard
    tours/page.tsx                  → List
    tours/create/page.tsx           → Form tambah
    tours/[id]/edit/page.tsx        → Form edit
    flights/...                     → (pola sama seperti tours)
    articles/...
    clients/...
    galleries/...
    hero-images/page.tsx            → List + form tambah (tanpa edit)
    hero-images/create/page.tsx
    team-members/...                → (pola sama seperti tours)
    users/...                       → (pola sama seperti tours)
    settings/page.tsx               → Form pengaturan situs

  api/
    auth/[...nextauth]/route.ts     → NextAuth handler
    upload/route.ts                 → Handler upload ke Supabase Storage (kalau tidak pakai Server Action)
```

Untuk operasi tulis (create/update/delete), disarankan pakai **Server Actions** Next.js langsung di tiap halaman admin (menggantikan peran `Route::resource` + Controller di Laravel), supaya tidak perlu bikin file route API terpisah untuk tiap CRUD.

## 10. Arsitektur Auth

- **Auth.js (NextAuth) Credentials Provider** — cocok karena tidak butuh OAuth pihak ketiga, sama seperti login custom di Laravel.
- Password di-hash pakai `bcrypt` (sama seperti Laravel), field `role` dicek di `session callback` untuk menentukan akses ke `/admin`.
- Proteksi route lewat `middleware.ts` (mengecek session + role, redirect ke `/login` kalau belum authenticated, mirip `EnsureAdminSession` di Laravel).
- Setelah login sukses, redirect otomatis ke `/admin` (bukan `intended URL` yang bisa nyangkut ke session lama — pernah jadi bug di versi Laravel karena pakai `redirect()->intended()`).

## 11. Arsitektur Upload File

- Pakai **Supabase Storage** (bucket `public`), diakses lewat `@supabase/supabase-js` di Server Action.
- Alur: form admin kirim `File` → Server Action upload ke Supabase Storage → dapat public URL → simpan URL itu ke kolom `imageUrl`/`photoUrl`/`logoUrl` di database (pola sama seperti `Storage::url()` di Laravel).
- Komponen `ImageUploadField` di React tetap dipertahankan (preview sebelum upload, dsb) — tinggal diadaptasi untuk memanggil Server Action alih-alih Inertia form.

## 12. Environment Variables yang Dibutuhkan

```
DATABASE_URL=               # connection string Supabase (pooler, port 6543)
DIRECT_URL=                 # connection string Supabase (direct, port 5432, khusus migration)
NEXTAUTH_SECRET=
NEXTAUTH_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=  # untuk upload dari server
```

## 13. Flow Pengerjaan (Fase per Fase)

| Fase | Deskripsi | Branch |
|---|---|---|
| 0 | Setup project: `create-next-app` (TypeScript, App Router, Tailwind), install semua dependency (Prisma, Supabase, NextAuth, dll), ESLint/Prettier, struktur folder | `feat/project-scaffold` |
| 1 | Setup Supabase project + Prisma schema + migration awal + seeder data demo | `feat/database-setup` |
| 2 | Setup Auth.js (Credentials Provider), middleware proteksi `/admin`, halaman Login | `feat/auth-setup` |
| 3 | Base layout: `MainLayout` (Navbar + Footer + WA bubble), `AdminLayout` (sidebar + navbar fixed) — terapkan Design System Bagian 5 di sini dari awal | `feat/base-layouts` |
| 4 | Halaman publik statis: About Us, Gallery, Contact | `feat/public-static-pages` |
| 5 | Halaman publik dinamis: Home (hero slider, tour/artikel section), Tour + Tour Detail | `feat/public-home-tour` |
| 6 | Halaman publik dinamis: Flight + Flight Detail, Article Detail | `feat/public-flight-article` |
| 7 | Admin: Dashboard + CRUD Tours, Flights | `feat/admin-tours-flights-crud` |
| 8 | Admin: CRUD Articles, Clients, Galleries | `feat/admin-content-crud` |
| 9 | Admin: CRUD Hero Images, Team Members (+ slider auto-advance 5 detik di About Us) | `feat/admin-hero-team-crud` |
| 10 | Admin: CRUD Users + proteksi self-delete/self-demote | `feat/admin-users-crud` |
| 11 | Admin: Settings (hero tiap halaman, About/Contact content, 2 nomor WhatsApp + tombol tes) | `feat/admin-settings` |
| 12 | Integrasi upload file ke Supabase Storage di semua form admin | `feat/supabase-storage-integration` |
| 13 | QA responsif (mobile/tablet/desktop per Bagian 7), aksesibilitas, cek ulang semua interaksi di Bagian 6 | `chore/qa-responsive-polish` |
| 14 | Deploy ke Vercel (staging), migrasi data dari MySQL lama ke Supabase, cutover DNS | `chore/deploy-vercel` |

**Konvensi branch:** sama seperti project Laravel — branch dari `dev`, merge ke `dev`, `main` hanya disentuh saat cutover produksi final.

## 14. Rencana Migrasi Data dari MySQL ke Supabase

Ini rencana konkret untuk Fase 14 (`chore/deploy-vercel`) — memindahkan **data asli** yang sudah ada di MySQL (tour, flight, artikel, klien, galeri, hero images, tim, users, site setting) ke Supabase, bukan cuma seed data demo.

1. **Export data lama dari MySQL**
   - Dump tiap tabel jadi JSON lewat Artisan (paling gampang, tidak perlu tool tambahan):
     ```bash
     php artisan tinker --execute="file_put_contents('tours.json', App\Models\Tour::all()->toJson());"
     # ulangi untuk Flight, Article, Client, Gallery, HeroImage, TeamMember, User, SiteSetting
     ```
   - Atau pakai `mysqldump` kalau mau backup mentah sebagai cadangan.

2. **Siapkan Supabase & jalankan migration Prisma**
   - Buat project baru di Supabase, ambil `DATABASE_URL` (pooler) dan `DIRECT_URL` (direct connection) dari menu Connect.
   - `npx prisma migrate deploy` di project Next.js untuk membuat semua tabel (skema dari Bagian 8) di Postgres Supabase.

3. **Import data ke Supabase lewat script seed Prisma**
   - Baca tiap file JSON hasil export, lalu `createMany` ke model Prisma yang sesuai. Karena skema Prisma pakai `Int @id @default(autoincrement())` yang sama seperti MySQL, ID lama bisa langsung dipakai ulang (insert eksplisit dengan `id` dari data lama), tidak perlu remap foreign key.
   - Setelah insert manual dengan ID eksplisit, jalankan reset sequence Postgres supaya auto-increment berikutnya tidak bentrok:
     ```sql
     SELECT setval('"Tour_id_seq"', (SELECT MAX(id) FROM "Tour"));
     -- ulangi untuk tabel lain yang match
     ```

4. **Pindahkan file upload (gambar) ke Supabase Storage**
   - Semua `imageUrl`/`photoUrl`/`logoUrl` di MySQL menyimpan path lokal (`/storage/...`). Perlu diunggah ulang file fisiknya (dari folder `storage/app/public` lama) ke bucket Supabase Storage, lalu update kolom URL di database supaya menunjuk ke URL Supabase yang baru.
   - Bisa ditulis sebagai satu script migrasi sekali-jalan (bukan bagian dari aplikasi Next.js itu sendiri).

5. **Verifikasi**
   - Bandingkan jumlah baris tiap tabel (MySQL vs Supabase) harus sama persis.
   - Spot-check beberapa data penting (tour unggulan, setting kontak, user admin) tampil benar di Next.js.
   - Cek semua gambar termuat (tidak ada 404) setelah pindah ke Supabase Storage.
   - Ulangi pengecekan interaksi di Bagian 6 (hero, slider, navbar, WA button) di data yang sudah asli, bukan cuma data demo.

6. **Cutover**
   - Setelah semua data dan gambar terverifikasi, arahkan domain ke deployment Vercel yang baru.
   - Matikan/arsipkan server Laravel lama setelah masa transisi aman (jangan langsung dihapus, simpan sebagai cadangan).

## 15. Yang Sengaja Tidak Ikut Migrasi

- Member login (sudah di-drop dari scope sejak awal project Laravel)
- Terjemahan otomatis ID/EN (masih tahap diskusi, belum diimplementasi)
- Pengiriman WhatsApp otomatis via API (tetap manual `wa.me` link sesuai keputusan sebelumnya)

## 16. Definition of Done

Migrasi dianggap selesai kalau:
1. Semua item di checklist Bagian 4 sudah ada dan berfungsi di Next.js
2. Semua perilaku interaktif di Bagian 6 (hero, slider, navbar, admin layout, dll) sudah direplikasi persis, bukan cuma "kelihatan mirip"
3. Semua data dari database lama sudah dipindahkan ke Supabase tanpa kehilangan data
4. Deploy di Vercel sukses, domain sudah diarahkan, dan admin bisa login serta mengelola konten seperti biasa
