diff --git a/UI_STYLE_GUIDE.md b/UI_STYLE_GUIDE.md new file mode 100644 index 0000000..e784709 --- /dev/null +++ b/UI_STYLE_GUIDE.md @@ -0,0 +1,198 @@ +# 🎨 SeTrip β€” UI Style Guide + +Panduan visual untuk membuat tampilan SeTrip terasa **natural, manusiawi, dan tidak "AI-generated"** β€” tanpa mengorbankan SEO. + +> Prinsip utama: **clean, calm, earthy.** SeTrip itu social-companion platform ("pergi bareng, bukan sendiri"), bukan marketplace booking. UI harus terasa hangat & tenang, bukan ramai & promosi. + +--- + +## 1. Filosofi Desain + +| Hindari (kesan AI-generated) | Gunakan (kesan natural) | +| --- | --- | +| ❌ Gradient berlebihan | βœ… Background putih bersih / `neutral-50` | +| ❌ Neumorphism | βœ… Soft green / earthy tone | +| ❌ Glassmorphism ekstrem | βœ… Border tipis 1px + shadow lembut | +| ❌ Icon 3D / emoji sebagai UI icon | βœ… Stroke icon tipis (lucide-react) | +| ❌ Card mengambang dengan blur tebal | βœ… Simple rounded card, datar, jelas | +| ❌ Warna saturasi tinggi di mana-mana | βœ… 1 warna aksen, sisanya netral | + +**Tiga kata kunci:** *bersih Β· tenang Β· jujur.* Kalau sebuah elemen terasa "ingin pamer", kemungkinan besar perlu disederhanakan. + +--- + +## 2. Warna + +Token warna sudah tersedia di [app/globals.css](app/globals.css) β€” **gunakan token, jangan hardcode hex.** + +| Peran | Token | Catatan | +| --- | --- | --- | +| Aksi utama / brand | `primary-600` (#16A34A) | Hijau gunung β€” earthy, tidak neon | +| Hover aksi utama | `primary-700` | Hindari `primary-500` (terlalu terang) untuk hover tombol | +| Aksen sekunder | `secondary-600` (#0EA5E9) | Pakai hemat β€” info, link, badge vibe | +| Teks utama | `neutral-800` | | +| Teks sekunder | `neutral-500` | | +| Border | `neutral-200` | Selalu 1px | +| Background halaman | `neutral-50` | | +| Surface / card | `white` | | + +### Aturan warna + +- **Satu aksen per layar.** Hijau adalah bintangnya. Biru hanya bumbu. +- **Maksimal 1 area gradient per halaman**, dan harus halus (mis. hero). Sisanya warna solid. +- Surface = putih solid. Jangan pakai `bg-white/80 + backdrop-blur` untuk card biasa. +- Earthy tone tambahan diperbolehkan sebagai background section (`primary-50`, `amber-50`) tapi jangan dijadikan blok besar warna-warni. + +--- + +## 3. Sistem Ikon β€” lucide-react + +`lucide-react` sudah terpasang. **Stroke icon = wajah baru SeTrip.** + +### Aturan ikon + +- **Stroke icon, bukan filled.** Lucide default sudah stroke β€” jangan ganti `fill`. +- Ukuran konsisten: `16` (inline teks), `20` (tombol/list), `24` (header section). +- Ketebalan stroke seragam: `strokeWidth={1.75}` (default lucide `2` sedikit terlalu tebal untuk gaya clean ini). +- Warna ikut teks: `text-neutral-500` untuk netral, `text-primary-600` untuk aktif. +- **Jangan** beri ikon background bulat berwarna + emoji di dalamnya (pola lama). Cukup ikon polos, atau ikon di atas lingkaran `neutral-100` yang sangat soft bila perlu penekanan. + +```tsx +import { Mountain } from "lucide-react"; + +// inline + + +// di tombol + +``` + +### Pemetaan ikon per fitur + +| Fitur | Ikon lucide | +| --- | --- | +| Trip | `Mountain` | +| Group / peserta | `Users` | +| Organizer | `BadgeCheck` | +| Verified | `ShieldCheck` | +| Payment | `Wallet` | +| Meeting Point | `MapPinned` | +| Chat | `MessageCircle` | +| Review / rating | `Star` | +| Profil | `UserRound` | + +Saran tambahan yang konsisten dengan set di atas: + +| Konteks | Ikon lucide | +| --- | --- | +| Tanggal / jadwal | `CalendarDays` | +| Lokasi umum | `MapPin` | +| Buat trip (FAB & CTA) | `Plus` | +| Cari / filter | `Search`, `SlidersHorizontal` | +| Menu mobile | `Menu` / `X` | +| Kategori (jelajah) | `Compass` | +| Sedang ramai / populer | `Flame` atau `TrendingUp` | +| Harga | `Tag` | + +> **Catatan emoji kategori:** `categoryMeta()` di [lib/activity-category.ts](lib/activity-category.ts) masih memakai emoji (πŸ”οΈπŸ•οΈπŸ€Ώ). Boleh dipertahankan **hanya** di konten data trip (terasa playful & manusiawi di tempat itu), tapi **elemen UI/chrome** (navbar, header section, tombol, badge status) harus pakai stroke icon. + +--- + +## 4. Komponen + +### Card + +``` +βœ… rounded-2xl Β· border border-neutral-200 Β· bg-white +βœ… hover: shadow lembut + translate-y-0.5 (sudah dipakai di TripCard β€” pertahankan) +❌ jangan: shadow tebal default, blur, gradient border +``` + +### Tombol + +| Jenis | Style | +| --- | --- | +| Primer | `bg-primary-600 hover:bg-primary-700 text-white rounded-xl` | +| Sekunder | `border border-neutral-200 text-neutral-700 hover:bg-neutral-50` | +| Ghost | `text-neutral-600 hover:bg-neutral-100` | + +- Shadow tombol seperlunya. `shadow-lg shadow-primary-600/25` boleh untuk **satu** CTA utama per layar, jangan semua tombol. +- `hover:scale-105` cukup untuk CTA hero saja β€” jangan di semua tombol (terasa "demo template"). +- Sertakan ikon lucide bila memperjelas aksi (mis. `Plus` untuk "Buat Trip"). + +### Badge / pill + +- `rounded-full`, teks kecil, warna soft (`primary-50`/`primary-700`). +- Status pakai warna semantik solid lembut, bukan transparan + blur. + +### Header section + +Pola lama: kotak berwarna + emoji. Pola baru: + +```tsx +
+ +
+

Jelajah per Kategori

+

Hiking, diving, konser, sampai retreat

+
+
+``` + +--- + +## 5. Yang Perlu Dirombak di Codebase + +Temuan konkret dari kode saat ini: + +| Lokasi | Masalah | Aksi | +| --- | --- | --- | +| [app/(public)/page.tsx](app/(public)/page.tsx) | Header section pakai kotak warna + emoji (✨πŸ”₯πŸ”οΈπŸ€), badge hero pakai emoji 🀝 | Ganti ke stroke icon (`Compass`, `Flame`, `Mountain`, `Users`) | +| [app/(public)/page.tsx](app/(public)/page.tsx#L110) | Hero gradient 3 warna (`from-primary-900 via-neutral-900 to-secondary-900`) | Sederhanakan jadi overlay solid `neutral-900/80` atau gradient 2 warna halus | +| [app/(public)/page.tsx](app/(public)/page.tsx#L386) | FAB pakai teks `"+"` | Ganti `` | +| [app/(public)/page.tsx](app/(public)/page.tsx#L153) | Stat "100% Seru" terasa filler/AI | Ganti metrik nyata (jumlah peserta, organizer terverifikasi) atau hapus | +| [components/shared/navbar.tsx](components/shared/navbar.tsx#L112) | Hamburger pakai inline SVG manual | Ganti `Menu` / `X` dari lucide | +| [components/shared/navbar.tsx](components/shared/navbar.tsx#L13) | `bg-white/90 backdrop-blur-md` | Boleh dipertahankan (tipis, wajar untuk sticky nav) β€” jangan ditebalkan | +| [features/trip/components/trip-card.tsx](features/trip/components/trip-card.tsx) | Avatar fallback & meta info bisa diperkuat dengan ikon stroke (`Users`, `CalendarDays`, `MapPin`) | Tambah ikon kecil di baris meta | + +Prioritas: **homepage dulu** (paling sering dilihat & paling kuat kesan AI-nya), lalu navbar, lalu komponen kartu. + +--- + +## 6. SEO β€” Wajib Dijaga + +Perubahan visual **tidak boleh** menurunkan SEO. Aturan: + +- **Ikon lucide = inline SVG**, ringan & tidak memblokir render. Aman untuk Core Web Vitals. +- **Ikon dekoratif** (hiasan di samping teks) harus `aria-hidden`. Lucide perlu di-set manual: + ```tsx + + ``` +- **Ikon yang berdiri sendiri sebagai tombol** (mis. tombol menu) wajib punya label: + ```tsx + + ``` +- **Jangan ubah teks jadi gambar.** Heading, slogan, deskripsi harus tetap teks HTML. +- **Pertahankan hirarki heading:** satu `

` per halaman, `

` untuk section. Jangan turunkan jadi `
` saat merapikan visual. +- **Pertahankan metadata & JSON-LD** di [app/layout.tsx](app/layout.tsx) dan [app/(public)/page.tsx](app/(public)/page.tsx) β€” structured data, OpenGraph, canonical jangan disentuh saat refactor UI. +- **Komponen tetap Server Component** kalau memungkinkan. Jangan tambah `"use client"` cuma untuk render ikon β€” lucide jalan di server. +- **Gambar:** terus pakai `next/image` dengan `alt` deskriptif dan `priority` untuk LCP (cover hero & kartu pertama). +- **Kontras warna** minimal AA: stroke icon `neutral-500` di atas putih sudah memenuhi; jangan pakai `neutral-300` untuk ikon/teks penting. + +--- + +## 7. Checklist Implementasi + +- [ ] Ganti semua emoji di chrome UI (navbar, header section, tombol, FAB) β†’ stroke icon lucide +- [ ] Standarkan `size` (16/20/24) & `strokeWidth={1.75}` di seluruh ikon +- [ ] Sederhanakan gradient hero homepage jadi maksimal 2 warna / overlay solid +- [ ] Ganti hamburger SVG manual di navbar β†’ `Menu`/`X` +- [ ] Tinjau metrik "100% Seru" β€” ganti angka nyata atau hapus +- [ ] Pastikan ikon dekoratif `aria-hidden`, ikon-tombol punya `aria-label` +- [ ] Pastikan struktur heading `h1`/`h2` tetap utuh setelah refactor +- [ ] Jalankan Lighthouse β€” skor SEO & Accessibility tidak turun +- [ ] Verifikasi tidak ada `"use client"` baru yang ditambahkan hanya demi ikon + +--- + +*Acuan token: [app/globals.css](app/globals.css) Β· Acuan brand: [lib/site.ts](lib/site.ts)* diff --git a/app/(public)/create-trip/page.tsx b/app/(public)/create-trip/page.tsx index 5fc8fb4..2b614ec 100644 --- a/app/(public)/create-trip/page.tsx +++ b/app/(public)/create-trip/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { Lock, Clock, CircleAlert } from "lucide-react"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { organizerService } from "@/server/services/organizer.service"; @@ -11,8 +12,13 @@ export default async function CreateTripPage() { return (
-
- πŸ”’ +
+

Kamu harus login untuk membuat trip. @@ -57,8 +63,9 @@ function VerificationBanner({ if (status === "PENDING") { return (

-

- ⏳ Verifikasi sedang diproses +

+ + Verifikasi sedang diproses

Pengajuan verifikasi-mu masih ditinjau admin. Sementara menunggu, kamu @@ -73,8 +80,9 @@ function VerificationBanner({

-

- ⚠️ {isRejected ? "Verifikasi ditolak" : "Belum terverifikasi"} +

+ + {isRejected ? "Verifikasi ditolak" : "Belum terverifikasi"}

{isRejected diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx index ee54476..dc0003e 100644 --- a/app/(public)/login/page.tsx +++ b/app/(public)/login/page.tsx @@ -84,7 +84,7 @@ function LoginForm() {

{/* Card */} -
+
{error && (
{error} diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx index 751e697..5cf0b6c 100644 --- a/app/(public)/page.tsx +++ b/app/(public)/page.tsx @@ -8,6 +8,15 @@ import { profileRepo } from "@/server/repositories/profile.repo"; import { TripCard } from "@/features/trip/components/trip-card"; import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site"; import { ACTIVITY_CATEGORIES, categoryMeta } from "@/lib/activity-category"; +import { + Compass, + Flame, + Mountain, + Handshake, + Tent, + Plus, + type LucideIcon, +} from "lucide-react"; type OpenTrip = Awaited>[number]; @@ -44,6 +53,9 @@ export default async function HomePage() { const now = new Date(); const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + // Social proof: total orang yang sudah gabung di seluruh open trip. + const joinerCount = trips.reduce((sum, t) => sum + t._count.participants, 0); + const upcomingTrips = trips .filter((t) => new Date(t.date) <= nextWeek) .slice(0, 3); @@ -107,12 +119,17 @@ export default async function HomePage() { className="object-cover opacity-10 brightness-150" priority /> -
+
{/* Brand badge */}
- 🀝 + Cari teman trip & aktivitas @@ -150,8 +167,12 @@ export default async function HomePage() {
-

100%

-

Seru

+

+ {joinerCount} +

+

+ Sudah Gabung +

@@ -161,19 +182,11 @@ export default async function HomePage() {
{/* Jelajah per kategori */}
-
-
- ✨ -
-
-

- Jelajah per Kategori -

-

- Hiking, diving, konser, sampai retreat -

-
-
+
{ACTIVITY_CATEGORIES.map((c) => { const m = categoryMeta(c); @@ -194,19 +207,11 @@ export default async function HomePage() { {/* Trip Terdekat */} {upcomingTrips.length > 0 && (
-
-
- πŸ”₯ -
-
-

- Trip Terdekat -

-

- Berangkat dalam 7 hari ke depan -

-
-
+
{upcomingTrips.slice(0, 3).map((trip, i) => ( -
-
-
- πŸ”οΈ -
-
-

- Open Trip -

-

- Pilih trip, ketemu teman baru -

-
-
- - Lihat semua - -
+ + Lihat semua + + } + /> {latestTrips.length === 0 ? (
-
- πŸ•οΈ +
+

Belum ada trip tersedia @@ -312,19 +314,11 @@ export default async function HomePage() { {/* Lagi Ramai β€” social proof, bukan price proof */} {buzzingTrips.length > 0 && (

-
-
- 🀝 -
-
-

- Lagi Ramai -

-

- Banyak yang sudah gabung β€” kamu nggak bakal jalan sendirian -

-
-
+
{buzzingTrips.map((trip) => ( - + +
); } + +/** Heading section homepage β€” ikon stroke + judul, opsional aksi di kanan. */ +function SectionHeading({ + icon: Icon, + title, + subtitle, + action, +}: { + icon: LucideIcon; + title: string; + subtitle?: string; + action?: React.ReactNode; +}) { + return ( +
+
+ +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+
+ {action} +
+ ); +} diff --git a/app/(public)/people/page.tsx b/app/(public)/people/page.tsx index f632e89..499b76f 100644 --- a/app/(public)/people/page.tsx +++ b/app/(public)/people/page.tsx @@ -5,6 +5,7 @@ import { UserCard } from "@/features/profile/components/user-card"; import { PeopleFilter } from "@/features/profile/components/people-filter"; import { isVibe, vibeLabel } from "@/lib/vibe"; import { siteConfig } from "@/lib/site"; +import { Users } from "lucide-react"; interface PeoplePageProps { searchParams: Promise<{ @@ -68,8 +69,13 @@ export default async function PeoplePage({ searchParams }: PeoplePageProps) { {people.length === 0 ? (
-
- πŸ” +
+

{hasFilters diff --git a/app/(public)/privacy/page.tsx b/app/(public)/privacy/page.tsx index f3bd8f5..572e0f0 100644 --- a/app/(public)/privacy/page.tsx +++ b/app/(public)/privacy/page.tsx @@ -1,12 +1,19 @@ import Link from "next/link"; +import { ShieldCheck, CircleCheck } from "lucide-react"; export default function PrivacyPage() { return (

-

- πŸ”’ Kebijakan Privasi SeTrip +

+ + Kebijakan Privasi SeTrip

Terakhir diperbarui: 2026-04-27 @@ -205,7 +212,15 @@ export default function PrivacyPage() {

-

βœ… Persetujuan

+

+ + Persetujuan +

Dengan menggunakan SeTrip, Anda menyatakan bahwa:

diff --git a/app/(public)/profile/page.tsx b/app/(public)/profile/page.tsx index b22bf24..4b8c26f 100644 --- a/app/(public)/profile/page.tsx +++ b/app/(public)/profile/page.tsx @@ -10,6 +10,7 @@ import { TripCard } from "@/features/trip/components/trip-card"; import { ProfileTripRow } from "@/features/profile/components/profile-trip-row"; import { ProfileEditor } from "@/features/profile/components/profile-editor"; import { EarningsSection } from "@/features/payout/components/earnings-section"; +import { Plus, ChevronRight } from "lucide-react"; export const metadata: Metadata = { title: "Profil Saya", @@ -81,9 +82,10 @@ export default async function ProfilePage() {
- + Buat trip + + Buat trip
@@ -133,13 +135,14 @@ export default async function ProfilePage() { endDate={t.endDate} rightSlot={ - {hasReview ? "Ubah ulasan β†’" : "Beri ulasan β†’"} + {hasReview ? "Ubah ulasan" : "Beri ulasan"} + } /> diff --git a/app/(public)/register/page.tsx b/app/(public)/register/page.tsx index a7c963b..cf5eef7 100644 --- a/app/(public)/register/page.tsx +++ b/app/(public)/register/page.tsx @@ -77,7 +77,7 @@ export default function RegisterPage() {
{/* Card */} -
+
{error && (
{error} diff --git a/app/(public)/terms/page.tsx b/app/(public)/terms/page.tsx index f2d3e83..213d8a2 100644 --- a/app/(public)/terms/page.tsx +++ b/app/(public)/terms/page.tsx @@ -1,12 +1,19 @@ import Link from "next/link"; +import { FileText, CircleCheck } from "lucide-react"; export default function TermsPage() { return (
-

- πŸ“œ Syarat & Ketentuan SeTrip +

+ + Syarat & Ketentuan SeTrip

Terakhir diperbarui: 2026-04-27 @@ -262,7 +269,15 @@ export default function TermsPage() {

-

βœ… Persetujuan

+

+ + Persetujuan +

Dengan menggunakan SeTrip, Anda menyatakan bahwa:

diff --git a/app/(public)/trips/[id]/page.tsx b/app/(public)/trips/[id]/page.tsx index 9f8ec9c..5d90737 100644 --- a/app/(public)/trips/[id]/page.tsx +++ b/app/(public)/trips/[id]/page.tsx @@ -28,6 +28,14 @@ import { isTripDepartureDayPast, } from "@/lib/trip-dates"; import { previewRefund } from "@/lib/refund-policy"; +import { + MapPin, + CalendarDays, + Wallet, + UserRound, + Zap, + Users, +} from "lucide-react"; export async function generateMetadata({ params, @@ -309,8 +317,13 @@ export default async function TripDetailPage({ {/* Info Grid */}
- - πŸ“ + +

Lokasi

@@ -319,8 +332,13 @@ export default async function TripDetailPage({
- - πŸ“… + +

Tanggal

@@ -331,8 +349,13 @@ export default async function TripDetailPage({
- - πŸ’° + +

Harga

@@ -343,8 +366,13 @@ export default async function TripDetailPage({
- - πŸ‘€ + +

Organizer

@@ -372,8 +400,9 @@ export default async function TripDetailPage({ Peserta {spotsLeft > 0 && spotsLeft <= 3 && ( - - ⚑ Tinggal {spotsLeft} spot! + + + Tinggal {spotsLeft} spot! )} {spotsLeft <= 0 && ( @@ -418,8 +447,14 @@ export default async function TripDetailPage({ )}

{confirmedCount > 0 && ( -

- πŸ‘₯ Sudah join:{" "} +

+ + Sudah join:{" "} {confirmedParticipants .slice(0, 3) @@ -547,7 +582,7 @@ export default async function TripDetailPage({ Belum ada peserta yang dikonfirmasi.{" "} {pendingParticipants.length > 0 ? "Cek permintaan join di atas untuk menyetujui peserta." - : "Jadilah yang pertama mendaftar! πŸŽ’"} + : "Jadilah yang pertama mendaftar!"}

) : (
    @@ -578,8 +613,14 @@ export default async function TripDetailPage({ {p.user.name}

    {city && ( -

    - πŸ“ {city} +

    + + {city}

    )} {interests.length > 0 && ( diff --git a/app/(public)/trips/[id]/payment/page.tsx b/app/(public)/trips/[id]/payment/page.tsx index 620c2ff..91d687d 100644 --- a/app/(public)/trips/[id]/payment/page.tsx +++ b/app/(public)/trips/[id]/payment/page.tsx @@ -11,6 +11,15 @@ import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates"; import { isFreeTrip } from "@/lib/trip-pricing"; import { categoryMeta } from "@/lib/activity-category"; import { MidtransPayButton } from "@/features/booking/components/midtrans-pay-button"; +import { + ArrowLeft, + CalendarDays, + MapPin, + PartyPopper, + CircleCheck, + Clock, + Check, +} from "lucide-react"; export const metadata: Metadata = { title: "Detail Pembayaran", @@ -82,8 +91,15 @@ export default async function PaymentPage({ params, searchParams }: PageProps) {

    {trip.title}

    -

    - πŸ“… {dateRange} Β· πŸ“ {trip.location} +

    + + + {dateRange} + + + + {trip.location} +

    Organizer:{" "} @@ -102,8 +118,12 @@ export default async function PaymentPage({ params, searchParams }: PageProps) { return (

    - - ← Kembali ke trip + + + Kembali ke trip
    @@ -170,8 +190,13 @@ function FreeTripSection({ }) { return (
    -
    - πŸŽ‰ +
    +

    Trip ini gratis @@ -184,10 +209,28 @@ function FreeTripSection({

    Status keikutsertaan

    -

    - {bookingStatus === "PAID" - ? "βœ… Terkonfirmasi sebagai peserta" - : "⏳ Menunggu persetujuan organizer"} +

    + {bookingStatus === "PAID" ? ( + <> + + Terkonfirmasi sebagai peserta + + ) : ( + <> + + Menunggu persetujuan organizer + + )}

    @@ -250,10 +293,15 @@ function PaidTripSection({ {canPay && } {isFullyPaid && ( -
    +
    +

    - βœ… Pembayaran kamu sudah terkonfirmasi. Sampai jumpa di trip - bareng{" "} + Pembayaran kamu sudah terkonfirmasi. Sampai jumpa di trip bareng{" "} {organizerName}!

    @@ -262,9 +310,10 @@ function PaidTripSection({
    - ← Kembali ke detail trip + + Kembali ke detail trip
    @@ -298,7 +347,11 @@ function PaymentTimeline({ : "bg-neutral-200 text-neutral-500" }`} > - {s.done ? "βœ“" : i + 1} + {s.done ? ( + + ) : ( + i + 1 + )} - + Buat Trip + + Buat Trip
    @@ -113,8 +115,22 @@ export default async function TripsPage({ searchParams }: TripsPageProps) { {trips.length === 0 ? (
    -
    - {hasFilters ? "πŸ”" : "πŸ•οΈ"} +
    + {hasFilters ? ( + + ) : ( + + )}

    {hasFilters diff --git a/app/(public)/u/[id]/page.tsx b/app/(public)/u/[id]/page.tsx index 020e9c9..e6163ff 100644 --- a/app/(public)/u/[id]/page.tsx +++ b/app/(public)/u/[id]/page.tsx @@ -11,6 +11,7 @@ import { OrganizerStatsPanel } from "@/features/profile/components/organizer-sta import { OrganizerReviewsList } from "@/features/review/components/organizer-reviews-list"; import { siteConfig } from "@/lib/site"; import { vibeMeta } from "@/lib/vibe"; +import { BadgeCheck, MapPin, AtSign } from "lucide-react"; interface PageProps { params: Promise<{ id: string }>; @@ -86,10 +87,11 @@ export default async function PublicProfilePage({ params }: PageProps) {

{isVerifiedOrganizer && ( - βœ… Verified Organizer + + Verified Organizer )} @@ -97,7 +99,8 @@ export default async function PublicProfilePage({ params }: PageProps) {
{profile?.city && ( - πŸ“ {profile.city} + + {profile.city} )} Bergabung sejak {memberSince} @@ -141,8 +144,8 @@ export default async function PublicProfilePage({ params }: PageProps) { rel="noopener noreferrer nofollow" className="mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-primary-600 hover:text-primary-700" > - πŸ“Έ - @{profile.instagram} + + {profile.instagram} )}
diff --git a/app/(public)/verify/page.tsx b/app/(public)/verify/page.tsx index 56ba83d..dec046a 100644 --- a/app/(public)/verify/page.tsx +++ b/app/(public)/verify/page.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; import Link from "next/link"; +import { Clock, RefreshCw, CircleX, ArrowLeft } from "lucide-react"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { organizerService } from "@/server/services/organizer.service"; @@ -72,8 +73,9 @@ export default async function VerifyPage() { {verification?.status === "PENDING" && !verification.reuploadRequested && (
-

- ⏳ Menunggu review admin +

+ + Menunggu review admin

Pengajuanmu sedang diproses. Kami akan memberitahu via email setelah selesai. @@ -83,8 +85,9 @@ export default async function VerifyPage() { {verification?.reuploadRequested && (

-

- πŸ”„ Admin minta kamu upload ulang +

+ + Admin minta kamu upload ulang

{verification.reuploadNote && (

@@ -117,7 +120,10 @@ export default async function VerifyPage() { {verification?.status === "REJECTED" && (

-

❌ Pengajuan ditolak

+

+ + Pengajuan ditolak +

{verification.rejectionReason && (

Alasan:{" "} @@ -135,8 +141,12 @@ export default async function VerifyPage() { verification?.reuploadRequested)) && }

- - ← Kembali ke profil + + + Kembali ke profil

diff --git a/app/admin/bookings/[id]/page.tsx b/app/admin/bookings/[id]/page.tsx index 3cd7e34..0c99e74 100644 --- a/app/admin/bookings/[id]/page.tsx +++ b/app/admin/bookings/[id]/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { ArrowLeft, CalendarDays, CircleAlert, MapPin } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { bookingRepo } from "@/server/repositories/booking.repo"; @@ -69,8 +70,12 @@ export default async function AdminBookingDetailPage({ params }: PageProps) { return (
- - ← Dashboard + + + Dashboard {booking.trip.title} -

- πŸ“… {formatTripCalendarDateRangeLong(booking.trip.date, booking.trip.endDate)}{" "} - Β· πŸ“ {booking.trip.destination}, {booking.trip.location} +

+ + {formatTripCalendarDateRangeLong(booking.trip.date, booking.trip.endDate)} + Β· + + {booking.trip.destination}, {booking.trip.location}

@@ -300,8 +318,14 @@ function PaymentEventCard({

)} {payment.rejectionReason && ( -

- ⚠️ {payment.rejectionReason} +

+ + {payment.rejectionReason}

)}
diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 621fb6b..78a37e5 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { Lock } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { AdminSidebar } from "@/components/admin/admin-sidebar"; @@ -31,7 +32,12 @@ export default async function AdminLayout({ return (
-

πŸ”’

+

Halaman khusus admin

diff --git a/app/admin/page.tsx b/app/admin/page.tsx index b258796..924d803 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,6 +1,7 @@ import { redirect } from "next/navigation"; import Link from "next/link"; import { getServerSession } from "next-auth"; +import { ChevronRight } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { organizerRepo } from "@/server/repositories/organizer.repo"; import { refundRepo } from "@/server/repositories/refund.repo"; @@ -155,8 +156,9 @@ export default async function AdminDashboardPage() { > {s.label} - - Buka β†’ + + Buka +

{s.value}

diff --git a/app/admin/system/page.tsx b/app/admin/system/page.tsx index ff5420a..cc9b400 100644 --- a/app/admin/system/page.tsx +++ b/app/admin/system/page.tsx @@ -1,6 +1,12 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { + ArrowUpRight, + CircleAlert, + CircleCheck, + CircleX, +} from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { prisma } from "@/lib/prisma"; @@ -112,8 +118,9 @@ export default async function AdminSystemPage() { {hasAnyStale && (
-

- ⚠️ Stale State Alerts +

+ + Stale State Alerts

    {stale.stalePaymentsCount > 0 && ( @@ -137,9 +144,10 @@ export default async function AdminSystemPage() { cron history di bawah.{" "} - Lihat HELD β†’ + Lihat HELD + )} @@ -149,9 +157,10 @@ export default async function AdminSystemPage() { > 7 hari belum di-process.{" "} - Lihat APPROVED β†’ + Lihat APPROVED + )} @@ -162,9 +171,10 @@ export default async function AdminSystemPage() { manual.{" "} - Lihat email gagal β†’ + Lihat email gagal + )} @@ -187,16 +197,23 @@ export default async function AdminSystemPage() { : "border-red-200 bg-red-50/50"; const badge = health === "ok" - ? { label: "🟒 OK", cls: "bg-emerald-100 text-emerald-800" } + ? { + label: "OK", + icon: CircleCheck, + cls: "bg-emerald-100 text-emerald-800", + } : health === "stale" ? { - label: "🟑 STALE", + label: "STALE", + icon: CircleAlert, cls: "bg-amber-100 text-amber-800", } : { - label: "πŸ”΄ FAILED", + label: "FAILED", + icon: CircleX, cls: "bg-red-100 text-red-800", }; + const BadgeIcon = badge.icon; return (
    + {badge.label}
@@ -276,9 +294,10 @@ export default async function AdminSystemPage() {

- Buka Email Log β†’ + Buka Email Log +

diff --git a/app/admin/trips/[id]/page.tsx b/app/admin/trips/[id]/page.tsx index 1b0e15f..8cc4f6d 100644 --- a/app/admin/trips/[id]/page.tsx +++ b/app/admin/trips/[id]/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { ArrowLeft, CalendarDays, MapPin } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { tripService } from "@/server/services/trip.service"; @@ -67,8 +68,12 @@ export default async function AdminTripDetailPage({ params }: PageProps) { return (
- - ← Kembali ke list trips + + + Kembali ke list trips
@@ -84,9 +89,22 @@ export default async function AdminTripDetailPage({ params }: PageProps) {

{trip.title}

-

- πŸ“… {formatTripCalendarDateRangeLong(trip.date, trip.endDate)} Β· - πŸ“ {trip.destination}, {trip.location} +

+ + {formatTripCalendarDateRangeLong(trip.date, trip.endDate)} + Β· + + {trip.destination}, {trip.location}

Organizer:{" "} @@ -220,8 +238,9 @@ export default async function AdminTripDetailPage({ params }: PageProps) { {p.user.name} {p.user.profile?.city && ( - - πŸ“ {p.user.profile.city} + + + {p.user.profile.city} )}

diff --git a/app/admin/trips/page.tsx b/app/admin/trips/page.tsx index af61631..d62da75 100644 --- a/app/admin/trips/page.tsx +++ b/app/admin/trips/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { CalendarDays, MapPin } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { tripRepo } from "@/server/repositories/trip.repo"; @@ -127,9 +128,22 @@ export default async function AdminTripsPage({ searchParams }: PageProps) {

{t.title}

-

- πŸ“… {formatTripCalendarDateRangeLong(t.date, t.endDate)} - {" Β· "}πŸ“ {t.location} +

+ + {formatTripCalendarDateRangeLong(t.date, t.endDate)} + Β· + + {t.location}

Organizer:{" "} diff --git a/app/admin/users/[id]/page.tsx b/app/admin/users/[id]/page.tsx index d6de8b3..f911e16 100644 --- a/app/admin/users/[id]/page.tsx +++ b/app/admin/users/[id]/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import Image from "next/image"; import { notFound, redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { ArrowLeft, ArrowUpRight, Ban, Check } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { userRepo } from "@/server/repositories/user.repo"; @@ -38,8 +39,12 @@ export default async function AdminUserDetailPage({ params }: PageProps) { return (

- - ← Kembali ke list users + + + Kembali ke list users
@@ -75,8 +80,9 @@ export default async function AdminUserDetailPage({ params }: PageProps) { )} {user.organizerVerification?.status === "APPROVED" && ( - - βœ“ Verified Organizer + + + Verified Organizer )}
@@ -122,8 +128,9 @@ export default async function AdminUserDetailPage({ params }: PageProps) { {user.suspended && (
-

- β›” Akun ditangguhkan +

+ + Akun ditangguhkan

{user.suspendedReason ?? "Tidak ada alasan tercatat."} @@ -244,9 +251,10 @@ export default async function AdminUserDetailPage({ params }: PageProps) { {" Β· "} - Buka di /admin/verifications β†’ + Buka di /admin/verifications +

{user.organizerVerification.rejectionReason && ( diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index ed5f51b..b3f4063 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import Image from "next/image"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { Check, ChartColumn } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { userRepo } from "@/server/repositories/user.repo"; @@ -56,9 +57,10 @@ export default async function AdminUsersPage({ searchParams }: PageProps) {
- πŸ“Š Stats + + Stats @@ -147,8 +149,9 @@ export default async function AdminUsersPage({ searchParams }: PageProps) { )} {u.organizerVerification?.status === "APPROVED" && ( - - βœ“ Organizer + + + Organizer )}
diff --git a/app/admin/users/stats/page.tsx b/app/admin/users/stats/page.tsx index dedf4ac..006a30f 100644 --- a/app/admin/users/stats/page.tsx +++ b/app/admin/users/stats/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; +import { ArrowLeft } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { prisma } from "@/lib/prisma"; @@ -97,8 +98,12 @@ export default async function AdminUserStatsPage() { return (
- - ← Kembali ke list users + + + Kembali ke list users
diff --git a/components/admin/admin-sidebar.tsx b/components/admin/admin-sidebar.tsx index eee9d7e..214fe9b 100644 --- a/components/admin/admin-sidebar.tsx +++ b/components/admin/admin-sidebar.tsx @@ -5,18 +5,33 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import Image from "next/image"; import { signOut } from "next-auth/react"; +import { + ArrowLeft, + ArrowUpRight, + Banknote, + Compass, + IdCard, + LayoutDashboard, + Mail, + Menu, + ScrollText, + Settings, + Users, + X, + type LucideIcon, +} from "lucide-react"; import { AdminSearchBar } from "@/features/admin/components/admin-search-bar"; -const NAV_ITEMS: { href: string; label: string; icon: string }[] = [ - { href: "/admin", label: "Dashboard", icon: "πŸ“Š" }, - { href: "/admin/trips", label: "Trips", icon: "🧭" }, - { href: "/admin/users", label: "Users", icon: "πŸ‘₯" }, - { href: "/admin/verifications", label: "Verifikasi", icon: "πŸͺͺ" }, - { href: "/admin/refunds", label: "Refund", icon: "↩️" }, - { href: "/admin/payouts", label: "Payout", icon: "πŸ’Έ" }, - { href: "/admin/emails", label: "Email", icon: "βœ‰οΈ" }, - { href: "/admin/audit-log", label: "Audit Log", icon: "πŸ“œ" }, - { href: "/admin/system", label: "System", icon: "βš™οΈ" }, +const NAV_ITEMS: { href: string; label: string; icon: LucideIcon }[] = [ + { href: "/admin", label: "Dashboard", icon: LayoutDashboard }, + { href: "/admin/trips", label: "Trips", icon: Compass }, + { href: "/admin/users", label: "Users", icon: Users }, + { href: "/admin/verifications", label: "Verifikasi", icon: IdCard }, + { href: "/admin/refunds", label: "Refund", icon: ArrowLeft }, + { href: "/admin/payouts", label: "Payout", icon: Banknote }, + { href: "/admin/emails", label: "Email", icon: Mail }, + { href: "/admin/audit-log", label: "Audit Log", icon: ScrollText }, + { href: "/admin/system", label: "System", icon: Settings }, ]; interface AdminSidebarProps { @@ -51,13 +66,9 @@ export function AdminSidebar({ user }: AdminSidebarProps) { aria-expanded={open} > {open ? ( - - - + ) : ( - - - + )} @@ -106,6 +117,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) { const isActive = pathname === item.href || (item.href !== "/admin" && pathname?.startsWith(item.href)); + const Icon = item.icon; return (
  • - - {item.icon} - + {item.label}
  • @@ -136,7 +146,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) { onClick={() => setOpen(false)} className="flex items-center gap-3 rounded-lg px-3 py-2 text-xs font-medium text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700" > - ↩ + Lihat situs publik diff --git a/components/shared/navbar.tsx b/components/shared/navbar.tsx index 822d538..70ee9c7 100644 --- a/components/shared/navbar.tsx +++ b/components/shared/navbar.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { useSession, signOut } from "next-auth/react"; +import { Menu, X } from "lucide-react"; export function Navbar() { const { data: session } = useSession(); @@ -109,29 +110,9 @@ export function Navbar() { aria-label="Toggle menu" > {menuOpen ? ( - - - + ) : ( - - - + )}
    diff --git a/components/shared/verified-badge.tsx b/components/shared/verified-badge.tsx index e008874..a8a0a17 100644 --- a/components/shared/verified-badge.tsx +++ b/components/shared/verified-badge.tsx @@ -1,23 +1,20 @@ +import { BadgeCheck } from "lucide-react"; + type Size = "sm" | "md"; export function VerifiedBadge({ size = "sm" }: { size?: Size }) { const cls = - size === "md" - ? "px-2.5 py-1 text-xs" - : "px-2 py-0.5 text-[10px]"; + size === "md" ? "px-2.5 py-1 text-xs" : "px-2 py-0.5 text-[10px]"; return ( - + Verified ); diff --git a/features/admin/components/export-csv-link.tsx b/features/admin/components/export-csv-link.tsx index 15fca4e..764abac 100644 --- a/features/admin/components/export-csv-link.tsx +++ b/features/admin/components/export-csv-link.tsx @@ -1,3 +1,5 @@ +import { Download } from "lucide-react"; + interface ExportCsvLinkProps { /** URL endpoint export, mis. `/api/admin/export/refunds`. */ href: string; @@ -22,7 +24,7 @@ export function ExportCsvLink({ className="inline-flex items-center gap-1.5 rounded-xl border border-neutral-200 bg-white px-3 py-1.5 text-xs font-semibold text-neutral-700 hover:bg-neutral-50" download > - ⬇️ + {label} ); diff --git a/features/admin/components/manual-verify-button.tsx b/features/admin/components/manual-verify-button.tsx index 6878ead..3c5dc2b 100644 --- a/features/admin/components/manual-verify-button.tsx +++ b/features/admin/components/manual-verify-button.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { Lock } from "lucide-react"; import { manualOverrideVerificationAction } from "@/features/organizer/actions"; interface ManualVerifyButtonProps { @@ -49,9 +50,10 @@ export function ManualVerifyButton({ ); } diff --git a/features/booking/components/admin-reconcile-button.tsx b/features/booking/components/admin-reconcile-button.tsx index 4e1e824..7d8ee7a 100644 --- a/features/booking/components/admin-reconcile-button.tsx +++ b/features/booking/components/admin-reconcile-button.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { Check } from "lucide-react"; import { adminReconcileMidtransAction } from "@/features/booking/actions"; interface AdminReconcileButtonProps { @@ -45,8 +46,9 @@ export function AdminReconcileButton({ {loading ? "Reconciling..." : "Reconcile Midtrans"} {status && ( - - βœ“ {reconcileOutcomeLabel(status)} + + + {reconcileOutcomeLabel(status)} )} {error && ( diff --git a/features/booking/components/cancel-booking-button.tsx b/features/booking/components/cancel-booking-button.tsx index 3c13c65..2cbdb79 100644 --- a/features/booking/components/cancel-booking-button.tsx +++ b/features/booking/components/cancel-booking-button.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { CircleAlert } from "lucide-react"; import { cancelBookingWithRefundAction } from "@/features/booking/actions"; import { formatRupiah } from "@/lib/utils"; @@ -112,9 +113,17 @@ export function CancelBookingButton({ tripId, preview }: CancelBookingButtonProp Tier: {preview.tierLabel}

    {noRefund ? ( -

    - ⚠️ Di luar window refund β€” uang tidak dikembalikan. Booking akan - di-cancel langsung. +

    + + + Di luar window refund β€” uang tidak dikembalikan. Booking akan + di-cancel langsung. +

    ) : (

    diff --git a/features/booking/components/copy-button.tsx b/features/booking/components/copy-button.tsx index 5543e5b..17bf9d1 100644 --- a/features/booking/components/copy-button.tsx +++ b/features/booking/components/copy-button.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { Check, Copy } from "lucide-react"; interface CopyButtonProps { value: string; @@ -24,9 +25,19 @@ export function CopyButton({ value, label = "Salin" }: CopyButtonProps) { ); } diff --git a/features/email/components/email-row-actions.tsx b/features/email/components/email-row-actions.tsx index 41ea9a0..2c19d5a 100644 --- a/features/email/components/email-row-actions.tsx +++ b/features/email/components/email-row-actions.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { Check } from "lucide-react"; import { retryEmailJobAction, resendEmailAction } from "@/features/email/actions"; const BTN_CLS = @@ -85,9 +86,18 @@ export function ResendEmailButton({ type="button" onClick={handleResend} disabled={loading || done} - className={BTN_CLS} + className={`${BTN_CLS} inline-flex items-center gap-1`} > - {loading ? "Mengirim…" : done ? "βœ“ Terkirim" : "Resend"} + {loading ? ( + "Mengirim…" + ) : done ? ( + <> + + Terkirim + + ) : ( + "Resend" + )} {error && (

    {error}

    diff --git a/features/organizer/components/review-card.tsx b/features/organizer/components/review-card.tsx index 749b2c7..ff614bc 100644 --- a/features/organizer/components/review-card.tsx +++ b/features/organizer/components/review-card.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { CircleCheck, CircleX, RefreshCw } from "lucide-react"; import { reopenVerificationAction, requestReuploadAction, @@ -180,9 +181,10 @@ export function ReviewCard({ verification }: { verification: Verification }) { type="button" onClick={() => setShowReopen(true)} disabled={loading} - className="rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50" + className="inline-flex items-center gap-1.5 rounded-xl border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-700 hover:bg-amber-50 disabled:opacity-50" > - πŸ”„ Buka kembali ke PENDING + + Buka kembali ke PENDING ) : (
    @@ -336,25 +338,28 @@ export function ReviewCard({ verification }: { verification: Verification }) { type="button" onClick={() => decide("APPROVED")} disabled={loading} - className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" + className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" > - βœ… Setujui + + Setujui
    )} diff --git a/features/organizer/components/verify-form.tsx b/features/organizer/components/verify-form.tsx index c6729ee..c9c146f 100644 --- a/features/organizer/components/verify-form.tsx +++ b/features/organizer/components/verify-form.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { IdCard, Image as ImageIcon, Landmark, Check } from "lucide-react"; import { submitVerificationAction } from "@/features/organizer/actions"; import { DateField } from "@/components/shared/date-picker"; @@ -79,7 +80,15 @@ export function VerifyForm({ initial }: { initial: Initial }) { )}
    -

    πŸ“‡ Data KTP

    +

    + + Data KTP +

    -

    πŸ–ΌοΈ Foto

    +

    + + Foto +

    Foto disimpan terenkripsi di server SeTrip dan hanya bisa dilihat oleh tim admin saat review. Maks 5MB, JPG/PNG/WebP. @@ -178,7 +195,15 @@ export function VerifyForm({ initial }: { initial: Initial }) {

    -

    🏦 Rekening Bank

    +

    + + Rekening Bank +

    {value && !busy && ( - βœ“ Terunggah + + + Terunggah + )}
    {previewUrl && ( diff --git a/features/payout/components/payout-review-card.tsx b/features/payout/components/payout-review-card.tsx index 049136a..23de943 100644 --- a/features/payout/components/payout-review-card.tsx +++ b/features/payout/components/payout-review-card.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { ArrowRight, Banknote, CircleAlert } from "lucide-react"; import { markPayoutPaidAction } from "@/features/payout/actions"; import { formatRupiah } from "@/lib/utils"; @@ -95,9 +96,10 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {

    - β†’ Lihat timeline booking + + Lihat timeline booking
    @@ -142,9 +144,18 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {

    ) : ( -

    - ⚠️ Organizer belum menyelesaikan verifikasi (KYC) β€” tidak ada rekening - snapshot. Hubungi organizer untuk konfirmasi rekening sebelum transfer. +

    + + + Organizer belum menyelesaikan verifikasi (KYC) β€” tidak ada + rekening snapshot. Hubungi organizer untuk konfirmasi rekening + sebelum transfer. +

    )}
    @@ -212,9 +223,10 @@ export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) { type="button" onClick={() => setOpen(true)} disabled={loading} - className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" + className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" > - πŸ’Έ Tandai sudah ditransfer ke organizer + + Tandai sudah ditransfer ke organizer )} diff --git a/features/profile/components/organizer-stats-panel.tsx b/features/profile/components/organizer-stats-panel.tsx index 5f182fe..ecbf1e6 100644 --- a/features/profile/components/organizer-stats-panel.tsx +++ b/features/profile/components/organizer-stats-panel.tsx @@ -1,3 +1,4 @@ +import { BadgeCheck, Star } from "lucide-react"; import type { OrganizerTrust } from "@/server/services/trust.service"; interface OrganizerStatsPanelProps { @@ -47,7 +48,8 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) { className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide text-primary-800" title="Identitas organizer telah diverifikasi (KTP & rekening)" > - βœ… Verified Organizer + + Verified Organizer )} {isTripLeader && ( @@ -83,7 +85,21 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) { /> + {avgRating} + + + ) : ( + "β€”" + ) + } subtitle={ reviewCount > 0 ? `${reviewCount} ulasan` @@ -107,8 +123,15 @@ export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) { key={star} className="flex items-center gap-2 text-xs" > - - {star} β˜… + + {star} +
    - Lihat publik β†— + Lihat publik +
    diff --git a/features/profile/components/user-card.tsx b/features/profile/components/user-card.tsx index 4a448a4..6322fef 100644 --- a/features/profile/components/user-card.tsx +++ b/features/profile/components/user-card.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import Link from "next/link"; +import { MapPin, BadgeCheck } from "lucide-react"; import { vibeMeta } from "@/lib/vibe"; import type { Vibe } from "@/app/generated/prisma/enums"; @@ -48,17 +49,19 @@ export function UserCard({ {name}

    {profile?.city && ( -

    - πŸ“ {profile.city} +

    + + {profile.city}

    )}
    {isVerifiedOrganizer && ( - βœ… Organizer + + Organizer )} {profile?.vibe && ( diff --git a/features/refund/components/refund-policy-section.tsx b/features/refund/components/refund-policy-section.tsx index 7d4d267..ec7614a 100644 --- a/features/refund/components/refund-policy-section.tsx +++ b/features/refund/components/refund-policy-section.tsx @@ -1,3 +1,4 @@ +import { LifeBuoy } from "lucide-react"; import { getRefundPolicyTiers } from "@/lib/refund-policy"; /** @@ -9,7 +10,13 @@ export function RefundPolicySection() { return (
    - πŸ›Ÿ Kebijakan refund saat peserta cancel + + Kebijakan refund saat peserta cancel

    diff --git a/features/refund/components/refund-review-card.tsx b/features/refund/components/refund-review-card.tsx index df0516b..82ccbdf 100644 --- a/features/refund/components/refund-review-card.tsx +++ b/features/refund/components/refund-review-card.tsx @@ -3,6 +3,13 @@ import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { + ArrowRight, + Banknote, + CircleAlert, + CircleCheck, + CircleX, +} from "lucide-react"; import { decideRefundAction } from "@/features/refund/actions"; import { formatRupiah } from "@/lib/utils"; @@ -129,9 +136,10 @@ export function RefundReviewCard({ refund }: { refund: RefundCardData }) {

    - β†’ Lihat timeline payment & refund + + Lihat timeline payment & refund
    setOpenAction("APPROVE")} disabled={loading} - className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" + className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" > - βœ… Setujui + + Setujui )} @@ -231,17 +241,19 @@ export function RefundReviewCard({ refund }: { refund: RefundCardData }) { type="button" onClick={() => setOpenAction("SUCCEEDED")} disabled={loading} - className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" + className="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50" > - πŸ’Έ Tandai sudah ditransfer + + Tandai sudah ditransfer )} diff --git a/features/review/components/organizer-reviews-list.tsx b/features/review/components/organizer-reviews-list.tsx index 7755885..ca0e189 100644 --- a/features/review/components/organizer-reviews-list.tsx +++ b/features/review/components/organizer-reviews-list.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import Image from "next/image"; +import { Star } from "lucide-react"; import type { OrganizerReviewItem } from "@/server/services/review.service"; interface OrganizerReviewsListProps { @@ -62,11 +63,22 @@ export function OrganizerReviewsList({ > {r.user.name} - - {"β˜…".repeat(r.rating)} - - {"β˜…".repeat(5 - r.rating)} - + + {[1, 2, 3, 4, 5].map((n) => ( + + ))}
    diff --git a/features/trip/components/admin-cancel-trip-button.tsx b/features/trip/components/admin-cancel-trip-button.tsx index b8d8e44..a07de5b 100644 --- a/features/trip/components/admin-cancel-trip-button.tsx +++ b/features/trip/components/admin-cancel-trip-button.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { CircleCheck } from "lucide-react"; import { adminCancelTripAction } from "@/features/trip/actions"; interface AdminCancelTripButtonProps { @@ -42,7 +43,10 @@ export function AdminCancelTripButton({ tripId }: AdminCancelTripButtonProps) { if (result) { return (
    -

    βœ… Trip berhasil dibatalkan.

    +

    + + Trip berhasil dibatalkan. +

    • β€’ {result.refundCount} booking PAID β†’ refund auto-dibuat
    • diff --git a/features/trip/components/create-trip-form.tsx b/features/trip/components/create-trip-form.tsx index 47a7c61..4e16b8b 100644 --- a/features/trip/components/create-trip-form.tsx +++ b/features/trip/components/create-trip-form.tsx @@ -2,6 +2,14 @@ import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; +import { + ArrowLeft, + ArrowRight, + Check, + X, + CircleAlert, + Users, +} from "lucide-react"; import { DateRangeField, TimeField } from "@/components/shared/date-picker"; import { createTripAction } from "@/features/trip/actions"; import { ImageUrlInput } from "@/features/trip/components/image-url-input"; @@ -367,9 +375,10 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) { type="button" onClick={goBack} disabled={step === 1 || loading} - className="rounded-xl border border-neutral-200 bg-white px-4 py-2.5 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-40" + className="inline-flex items-center gap-1 rounded-xl border border-neutral-200 bg-white px-4 py-2.5 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-40" > - ← Kembali + + Kembali {isLastStep ? ( @@ -388,9 +397,10 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) { )}
    @@ -442,7 +452,11 @@ function Stepper({ : "cursor-not-allowed" }`} > - {isCompleted ? "βœ“" : s.id} + {isCompleted ? ( + + ) : ( + s.id + )}
    @@ -1032,14 +1046,7 @@ function StepSchedule({
    - - - +
    {blockedByVerification && ( -

    - ⚠️ Trip berbayar butuh verifikasi organizer terlebih dahulu. +

    + + Trip berbayar butuh verifikasi organizer terlebih dahulu.

    )} diff --git a/features/trip/components/image-gallery.tsx b/features/trip/components/image-gallery.tsx index 4526600..6674a10 100644 --- a/features/trip/components/image-gallery.tsx +++ b/features/trip/components/image-gallery.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import { useState } from "react"; +import { Mountain } from "lucide-react"; interface TripImage { id: string; @@ -14,8 +15,13 @@ export function ImageGallery({ images }: { images: TripImage[] }) { if (images.length === 0) { return ( -
    - πŸ”οΈ +
    +
    ); } diff --git a/features/trip/components/image-url-input.tsx b/features/trip/components/image-url-input.tsx index 7580cad..16e6d15 100644 --- a/features/trip/components/image-url-input.tsx +++ b/features/trip/components/image-url-input.tsx @@ -1,5 +1,6 @@ "use client"; +import { Plus, X } from "lucide-react"; import { LIMITS } from "@/lib/limits"; interface ImageUrlInputProps { @@ -59,7 +60,7 @@ export function ImageUrlInput({ value, onChange }: ImageUrlInputProps) { aria-label={`Hapus foto ${i + 1}`} className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-neutral-200 text-neutral-400 hover:bg-red-50 hover:text-red-500" > - βœ• + )}
    @@ -71,7 +72,8 @@ export function ImageUrlInput({ value, onChange }: ImageUrlInputProps) { onClick={addField} className="mt-2 flex items-center gap-1 rounded-lg px-2 py-1 text-sm font-medium text-secondary-600 hover:bg-secondary-50" > - + Tambah foto + + Tambah foto )}

    diff --git a/features/trip/components/join-trip-button.tsx b/features/trip/components/join-trip-button.tsx index 839480b..35f36f2 100644 --- a/features/trip/components/join-trip-button.tsx +++ b/features/trip/components/join-trip-button.tsx @@ -143,7 +143,7 @@ export function JoinTripButton({ Kamu sudah{" "} terkonfirmasi sebagai peserta trip ini - {isFree && β€” trip gratis, tidak ada pembayaran πŸŽ‰}. + {isFree && β€” trip gratis, tidak ada pembayaran}. )} {needsPayment && ( diff --git a/features/trip/components/organizer-trust-panel.tsx b/features/trip/components/organizer-trust-panel.tsx index 04fc909..c7befbe 100644 --- a/features/trip/components/organizer-trust-panel.tsx +++ b/features/trip/components/organizer-trust-panel.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import { BadgeCheck, Star } from "lucide-react"; import type { OrganizerTrust } from "@/server/services/trust.service"; interface OrganizerTrustPanelProps { @@ -13,7 +14,7 @@ export function OrganizerTrustPanel({ trust, }: OrganizerTrustPanelProps) { return ( -

    +

    Organizer & kepercayaan

    @@ -42,7 +43,8 @@ export function OrganizerTrustPanel({ className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800 sm:text-xs" title="Identitas organizer telah diverifikasi (KTP & rekening)" > - βœ… Verified Organizer + + Verified Organizer )} {trust.isTripLeader && ( @@ -79,8 +81,20 @@ export function OrganizerTrustPanel({

    Rating organizer

    -

    - {trust.avgRating != null ? `${trust.avgRating} β˜…` : "β€”"} +

    + {trust.avgRating != null ? ( + <> + {trust.avgRating} + + + ) : ( + "β€”" + )}

    {trust.reviewCount > 0 && (

    diff --git a/features/trip/components/trip-card.tsx b/features/trip/components/trip-card.tsx index c898316..e5c5bf6 100644 --- a/features/trip/components/trip-card.tsx +++ b/features/trip/components/trip-card.tsx @@ -1,5 +1,12 @@ import Image from "next/image"; import Link from "next/link"; +import { + MapPin, + CalendarDays, + UserRound, + BadgeCheck, + Sparkles, +} from "lucide-react"; import { formatRupiah } from "@/lib/utils"; import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates"; import { categoryMeta } from "@/lib/activity-category"; @@ -132,21 +139,38 @@ export function TripCard({

    - πŸ“ {location} + + {location}
    - πŸ“…{" "} - {formatTripCalendarDateRangeLong(date, endDate)} + + {formatTripCalendarDateRangeLong(date, endDate)}
    - πŸ‘€{" "} + {organizerName} {isVerifiedOrganizer && ( - βœ… Verified + + Verified )} {isSmallGroup && ( @@ -193,10 +217,11 @@ export function TripCard({ )} {overlapCount > 0 && ( - ✨ {overlapCount} peserta sama minat + + {overlapCount} peserta sama minat )}
    diff --git a/features/trip/components/trip-filter.tsx b/features/trip/components/trip-filter.tsx index 286f70d..36d8af2 100644 --- a/features/trip/components/trip-filter.tsx +++ b/features/trip/components/trip-filter.tsx @@ -2,6 +2,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; +import { Search } from "lucide-react"; import { DateRangeField } from "@/components/shared/date-picker"; import { formatLocalCalendarYmd, @@ -263,18 +264,7 @@ export function TripFilter() {
    - - - +