"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { DateRangeField, TimeField } from "@/components/shared/date-picker"; import { createTripAction } from "@/features/trip/actions"; import { ImageUrlInput } from "@/features/trip/components/image-url-input"; import { formatLocalCalendarYmd } from "@/lib/trip-dates"; import { ACTIVITY_CATEGORIES, categoryMeta } from "@/lib/activity-category"; import { VIBES, vibeMeta } from "@/lib/vibe"; import { LIMITS } from "@/lib/limits"; import { isValidTimeFormat, timeToMinutes, type ItineraryItemInput, } from "@/lib/itinerary"; import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums"; type Step = 1 | 2 | 3 | 4; type DraftItineraryItem = { startTime: string; endTime: string; activity: string; }; type ItineraryDays = DraftItineraryItem[][]; type FormState = { category: ActivityCategory; vibe: Vibe | null; title: string; destination: string; location: string; description: string; meetingPoint: string; itineraryDays: ItineraryDays; whatsIncluded: string; whatsExcluded: string; imageUrls: string[]; maxParticipants: string; priceDisplay: string; }; const INITIAL_STATE: FormState = { category: "HIKING", vibe: null, title: "", destination: "", location: "", description: "", meetingPoint: "", itineraryDays: [], whatsIncluded: "", whatsExcluded: "", imageUrls: [""], maxParticipants: "", priceDisplay: "", }; function flattenItinerary(days: ItineraryDays): ItineraryItemInput[] { const out: ItineraryItemInput[] = []; days.forEach((dayItems, dayIdx) => { dayItems.forEach((item) => { if ( !item.startTime && !item.endTime && item.activity.trim().length === 0 ) { return; } out.push({ day: dayIdx + 1, startTime: item.startTime, endTime: item.endTime ? item.endTime : null, activity: item.activity, }); }); }); return out; } const STEPS: { id: Step; label: string; subtitle: string }[] = [ { id: 1, label: "Vibe", subtitle: "Jenis aktivitas & vibe" }, { id: 2, label: "Info", subtitle: "Judul, destinasi & lokasi" }, { id: 3, label: "Detail", subtitle: "Itinerary & foto" }, { id: 4, label: "Jadwal", subtitle: "Tanggal, peserta & harga" }, ]; const INPUT_CLS = "w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"; function formatRupiahInput(value: string): string { const num = value.replace(/\D/g, ""); return num.replace(/\B(?=(\d{3})+(?!\d))/g, "."); } function parseRupiahInput(value: string): string { return value.replace(/\./g, ""); } interface CreateTripFormProps { isVerifiedOrganizer: boolean; } export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) { const router = useRouter(); const [step, setStep] = useState(1); const [maxStepReached, setMaxStepReached] = useState(1); const [stepError, setStepError] = useState(""); const [submitError, setSubmitError] = useState(""); const [loading, setLoading] = useState(false); const [state, setState] = useState(INITIAL_STATE); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const meta = categoryMeta(state.category); const priceNumber = useMemo( () => Number(parseRupiahInput(state.priceDisplay) || "0"), [state.priceDisplay] ); const isPaidTrip = priceNumber > 0; const blockedByVerification = isPaidTrip && !isVerifiedOrganizer; function update(key: K, value: FormState[K]) { setState((prev) => ({ ...prev, [key]: value })); } function validateStep(target: Step): string | null { if (target === 1) { if (!state.category) return "Pilih kategori aktivitas dulu"; return null; } if (target === 2) { if (state.title.trim().length < 3) return "Judul minimal 3 karakter"; if (state.title.trim().length > LIMITS.MAX_TITLE_LENGTH) { return `Judul maksimal ${LIMITS.MAX_TITLE_LENGTH} karakter`; } if (state.destination.trim().length < 2) { return `${meta.destinationLabel} harus diisi`; } if (state.location.trim().length < 2) return "Lokasi harus diisi"; return null; } if (target === 3) { const hasInvalidUrl = state.imageUrls .map((u) => u.trim()) .filter(Boolean) .some((u) => { try { const parsed = new URL(u); return parsed.protocol !== "http:" && parsed.protocol !== "https:"; } catch { return true; } }); if (hasInvalidUrl) return "Ada URL foto yang tidak valid (harus http/https)"; for (let d = 0; d < state.itineraryDays.length; d++) { const dayItems = state.itineraryDays[d]; for (const item of dayItems) { const hasAnyInput = item.startTime.trim() || item.endTime.trim() || item.activity.trim(); if (!hasAnyInput) continue; if (!isValidTimeFormat(item.startTime)) { return `Hari ${d + 1}: jam mulai harus HH:mm`; } if (item.endTime && !isValidTimeFormat(item.endTime)) { return `Hari ${d + 1}: jam selesai harus HH:mm`; } if ( item.endTime && timeToMinutes(item.endTime) < timeToMinutes(item.startTime) ) { return `Hari ${d + 1}: jam selesai tidak boleh sebelum jam mulai`; } if (item.activity.trim().length === 0) { return `Hari ${d + 1}: deskripsi aktivitas harus diisi`; } if ( item.activity.trim().length > LIMITS.MAX_ITINERARY_ACTIVITY_LENGTH ) { return `Aktivitas maksimal ${LIMITS.MAX_ITINERARY_ACTIVITY_LENGTH} karakter`; } } } const totalItems = flattenItinerary(state.itineraryDays).length; if (totalItems > LIMITS.MAX_ITINERARY_ITEMS) { return `Maksimal ${LIMITS.MAX_ITINERARY_ITEMS} aktivitas total`; } return null; } if (target === 4) { if (!startDate) return "Tanggal berangkat harus diisi"; const max = Number(state.maxParticipants); if (!Number.isFinite(max) || max < LIMITS.MIN_PARTICIPANTS) { return `Minimal ${LIMITS.MIN_PARTICIPANTS} peserta`; } if (max > LIMITS.MAX_PARTICIPANTS) { return `Maksimal ${LIMITS.MAX_PARTICIPANTS} peserta`; } if (priceNumber < 0) return "Harga tidak valid"; if (priceNumber > LIMITS.MAX_PRICE_IDR) { return `Harga maksimal Rp ${LIMITS.MAX_PRICE_IDR.toLocaleString("id-ID")}`; } if (blockedByVerification) { return "Trip berbayar butuh verifikasi organizer terlebih dahulu"; } return null; } return null; } function goNext() { const err = validateStep(step); if (err) { setStepError(err); return; } setStepError(""); const next = Math.min(step + 1, STEPS.length) as Step; setStep(next); if (next > maxStepReached) setMaxStepReached(next); } function goBack() { setStepError(""); setStep((s) => (Math.max(1, s - 1) as Step)); } function jumpTo(target: Step) { if (target > maxStepReached) return; setStepError(""); setStep(target); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSubmitError(""); for (const s of [1, 2, 3, 4] as Step[]) { const err = validateStep(s); if (err) { setStep(s); setStepError(err); if (s > maxStepReached) setMaxStepReached(s); return; } } setLoading(true); const fd = new FormData(); fd.set("category", state.category); fd.set("title", state.title.trim()); fd.set("destination", state.destination.trim()); fd.set("location", state.location.trim()); fd.set("description", state.description.trim()); fd.set("meetingPoint", state.meetingPoint.trim()); fd.set("whatsIncluded", state.whatsIncluded.trim()); fd.set("whatsExcluded", state.whatsExcluded.trim()); fd.set("maxParticipants", state.maxParticipants); fd.set("price", parseRupiahInput(state.priceDisplay) || "0"); if (state.vibe) fd.set("vibe", state.vibe); if (startDate) fd.set("date", formatLocalCalendarYmd(startDate)); if (endDate && startDate) { const startYmd = formatLocalCalendarYmd(startDate); const endYmd = formatLocalCalendarYmd(endDate); if (endYmd !== startYmd) fd.set("endDate", endYmd); } for (const url of state.imageUrls.map((u) => u.trim()).filter(Boolean)) { fd.append("imageUrls", url); } const itineraryItems = flattenItinerary(state.itineraryDays); if (itineraryItems.length > 0) { fd.set("itineraryItems", JSON.stringify(itineraryItems)); } const result = await createTripAction(fd); setLoading(false); if (result.error) { setSubmitError(result.error); return; } if (result.tripId) { router.push(`/trips/${result.tripId}`); } } const isLastStep = step === STEPS.length; return (
{step === 1 && ( update("category", c)} onVibeChange={(v) => update("vibe", v)} /> )} {step === 2 && ( )} {step === 3 && ( )} {step === 4 && ( { setStartDate(s); setEndDate(e); }} onMaxParticipantsChange={(v) => update("maxParticipants", v)} onPriceChange={(v) => update("priceDisplay", v)} summary={{ ...state, startDate, endDate, priceNumber, }} /> )} {(stepError || submitError) && (
{stepError || submitError}
)}
{isLastStep ? ( ) : ( )}
); } function Stepper({ current, maxReached, onJump, }: { current: Step; maxReached: Step; onJump: (s: Step) => void; }) { const activeMeta = STEPS[current - 1]; return (
    {STEPS.map((s, idx) => { const isActive = s.id === current; const isCompleted = s.id < current; const canJump = s.id <= maxReached; const isLast = idx === STEPS.length - 1; const circleCls = isCompleted ? "bg-primary-600 text-white border-primary-600" : isActive ? "bg-primary-600 text-white border-primary-600 ring-4 ring-primary-100" : "bg-white text-neutral-400 border-neutral-200"; return (
  1. {!isLast && (
    )}
  2. ); })}

Langkah {current} dari {STEPS.length}

{activeMeta.label}

{activeMeta.subtitle}

); } function StepVibe({ category, vibe, onCategoryChange, onVibeChange, }: { category: ActivityCategory; vibe: Vibe | null; onCategoryChange: (c: ActivityCategory) => void; onVibeChange: (v: Vibe | null) => void; }) { return (
{ACTIVITY_CATEGORIES.map((c) => { const m = categoryMeta(c); const active = c === category; return ( ); })}

Bantu calon peserta menilai apakah ritmenya cocok dengan mereka.

{VIBES.map((v) => { const m = vibeMeta(v); const active = v === vibe; return ( ); })}
{vibe && (

{vibeMeta(vibe).description}

)}
); } function StepInfo({ title, destination, location, description, destinationLabel, destinationPlaceholder, onChange, }: { title: string; destination: string; location: string; description: string; destinationLabel: string; destinationPlaceholder: string; onChange: (key: K, value: FormState[K]) => void; }) { return (
onChange("title", e.target.value)} maxLength={LIMITS.MAX_TITLE_LENGTH} required className={INPUT_CLS} placeholder="contoh: Open Trip Papandayan Weekend" />
onChange("destination", e.target.value)} maxLength={LIMITS.MAX_DESTINATION_LENGTH} required className={INPUT_CLS} placeholder={destinationPlaceholder} />
onChange("location", e.target.value)} maxLength={LIMITS.MAX_LOCATION_LENGTH} required className={INPUT_CLS} placeholder="Garut, Jawa Barat" />