"use client"; import { useQuery } from "@tanstack/react-query"; import { apiClient } from "@/lib/api-client"; import Link from "next/link"; import Image from "next/image"; import { MapPin, ArrowRight, Shield, Train, Bus, CheckCircle2, Calendar, Clock, } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── interface PriceTier { id: string; priceMinor: number; currency: string; availableSeats: number; } interface Station { name: string; city: string; code: string; } interface Schedule { originStation: Station; destinationStation: Station; departureAt: string; durationMinutes: number; } interface HolidayPackage { id: string; code: string; name: string; description?: string | null; status: string; departureTime: string; validFrom: string; validUntil: string; totalCapacity: number; bookedCount: number; includedServices?: string[]; busTransferIncluded?: boolean; busTransferRoute?: string | null; journeyType?: "ONE_WAY" | "ROUND_TRIP"; returnSchedule?: Schedule | null; outboundSchedule?: Schedule; priceTiers: PriceTier[]; } // ─── Helpers ────────────────────────────────────────────────────────────────── // Pinned to Ethiopian time (EAT - UTC+3) — the zone train schedules are stored/computed // in server-side — so this matches every other booking page and the voucher PDF // regardless of the viewing device's own timezone. function fmtDate(iso: string, opts?: Intl.DateTimeFormatOptions): string { try { return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", ...opts, timeZone: "Africa/Addis_Ababa", }); } catch { return iso; } } function validityRange(from: string, until: string): string { return `${fmtDate(from, { month: "short", day: "numeric" })} – ${fmtDate(until, { month: "short", day: "numeric", year: "numeric" })}`; } function daysUntil(iso: string): number { return Math.max( 0, Math.floor((new Date(iso).getTime() - Date.now()) / 86400000), ); } function minPrice( tiers: PriceTier[], multiplier = 1, ): { amount: number; currency: string } | null { if (!tiers?.length) return null; const min = tiers.reduce((a, b) => (a.priceMinor < b.priceMinor ? a : b)); return { amount: (min.priceMinor * multiplier) / 100, currency: min.currency, }; } function fmtPrice(minor: number, currency: string): string { return `${currency} ${(minor / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function stripBullet(s: string): string { return s.replace(/^[•\-\t\s]+/, "").trim(); } // ─── Urgency badge ──────────────────────────────────────────────────────────── function UrgencyBadge({ until }: { until: string }) { const days = daysUntil(until); if (days > 14) return null; return ( {days === 0 ? "Last day!" : `Closes in ${days}d`} ); } // ─── Availability bar ───────────────────────────────────────────────────────── function AvailBar({ booked, total }: { booked: number; total: number }) { const pct = total > 0 ? Math.round(((total - booked) / total) * 100) : 100; const low = pct <= 20; return (
{total - booked} of {total} seats {pct}% available
); } // ─── Featured Card (first package — full-width, image left) ─────────────────── function FeaturedCard({ pkg }: { pkg: HolidayPackage }) { const multiplier = pkg.journeyType === "ROUND_TRIP" ? 2 : 1; const price = minPrice(pkg.priceTiers, multiplier); const origin = pkg.outboundSchedule?.originStation; const dest = pkg.outboundSchedule?.destinationStation; const days = daysUntil(pkg.validUntil); return (
{/* ── Left: Image ── */}
{pkg.name} {/* Gradient overlay */}
{/* Top-left badges */}
✦ Featured Package
{/* Bottom-left: round trip tag */} {pkg.returnSchedule && (
Round Trip
)}
{/* ── Right: Content ── */}
{/* Top section */}
{/* Status */} {pkg.status === "ACTIVE" && ( Booking Open )} {/* Name */}

{pkg.name?.trim()}

{/* Route */} {origin && dest && (
{origin.name?.trim()} {dest.name?.trim()} {pkg.busTransferIncluded && ( <> + {pkg.busTransferRoute?.trim() ?? "Bus transfer"} )}
)} {/* Info grid */}
} label="Validity" > {validityRange(pkg.validFrom, pkg.validUntil)} } label="Booking closes" > {fmtDate(pkg.validUntil, { month: "short", day: "numeric", year: "numeric", })} {days <= 14 && ` (${days}d left)`}
{/* Service chips */} {pkg.includedServices && pkg.includedServices.length > 0 && (
{pkg.includedServices.slice(0, 4).map((svc, i) => ( {stripBullet(svc).split(/\s+/).slice(0, 5).join(" ")} ))} {pkg.includedServices.length > 4 && ( +{pkg.includedServices.length - 4} more included )}
)}
{/* Bottom: price + CTA */}
{price ? (

Starting from

{price.currency}{" "} {price.amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, })}

{pkg.priceTiers.length} seat class {pkg.priceTiers.length !== 1 ? "es" : ""} available

) : (
)} View Package
); } // ─── Regular Package Card ───────────────────────────────────────────────────── function PackageCard({ pkg }: { pkg: HolidayPackage }) { const multiplier = pkg.journeyType === "ROUND_TRIP" ? 2 : 1; const price = minPrice(pkg.priceTiers, multiplier); const origin = pkg.outboundSchedule?.originStation; const dest = pkg.outboundSchedule?.destinationStation; return (
{/* Image / Gradient */}
🌍
{pkg.returnSchedule && (
↔ Round Trip
)}
{/* Content */}

{pkg.name?.trim()}

{/* Route */} {origin && dest && (
{origin.name?.trim()} {dest.name?.trim()}
)} {/* Validity */}
{validityRange(pkg.validFrom, pkg.validUntil)}
{/* Departure */}
Departs{" "} {fmtDate(pkg.departureTime, { weekday: "short", month: "short", day: "numeric", })}
{/* Availability bar */}
{/* Price + CTA */}
{price ? (

From

{fmtPrice(price.amount * 100, price.currency)}

) : (
)} View
); } // ─── Skeletons ──────────────────────────────────────────────────────────────── function FeaturedSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
{[1, 2, 3].map((i) => (
))}
); } function CardSkeleton() { return (
); } // ─── Info Pill (for featured card) ──────────────────────────────────────────── function InfoPill({ icon, label, children, }: { icon: React.ReactNode; label: string; children: React.ReactNode; }) { return (
{icon}

{label}

{children}

); } // ─── Section ────────────────────────────────────────────────────────────────── export default function PackagesSection() { const { data: packages, isLoading, isError, } = useQuery({ queryKey: ["packages"], queryFn: async () => (await apiClient.get("/packages")) as HolidayPackage[], }); if (isError) return null; const [featured, ...rest] = packages ?? []; return (
{/* Section header */}
🏖️

Holiday Packages

{/* Featured card */} {isLoading ? ( ) : featured ? ( ) : null} {/* Rest grid — 3 col desktop / 2 col tablet / 1 col mobile */} {isLoading ? (
{[1, 2, 3].map((i) => ( ))}
) : rest.length > 0 ? (
{rest.map((pkg) => ( ))}
) : null} {/* Empty state */} {!isLoading && !isError && !packages?.length && (
🏖️

No holiday packages available right now. Check back soon.

)}
); }