Files
setrip/app/trips/[id]/page.tsx
T
2026-04-17 00:16:31 +07:00

217 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import Link from "next/link";
import { authOptions } from "@/lib/auth";
import { tripService } from "@/server/services/trip.service";
import { formatRupiah, formatDateRange } from "@/lib/utils";
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
import { ImageGallery } from "@/features/trip/components/image-gallery";
export default async function TripDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const session = await getServerSession(authOptions);
let trip;
try {
trip = await tripService.getTripById(id);
} catch {
notFound();
}
const activeParticipants = trip.participants.filter(
(p) => p.status !== "CANCELLED"
);
const participantCount = activeParticipants.length;
const spotsLeft = trip.maxParticipants - participantCount;
const fillPercent = Math.min(
(participantCount / trip.maxParticipants) * 100,
100
);
const isOrganizer = session?.user?.id === trip.organizerId;
const currentParticipation = session?.user
? trip.participants.find(
(p) => p.userId === session.user.id && p.status !== "CANCELLED"
)
: null;
return (
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
{/* Breadcrumb */}
<div className="mb-3 flex items-center gap-2 text-xs text-neutral-500 sm:mb-4 sm:text-sm">
<Link href="/trips" className="hover:text-primary-600">
Open Trip
</Link>
<span>/</span>
<span className="truncate text-neutral-700">{trip.mountain}</span>
</div>
<div className="overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm">
{/* Image Gallery */}
<ImageGallery images={trip.images} />
{/* Title bar */}
<div className="border-b border-neutral-100 px-4 py-3 sm:px-6 sm:py-4">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h1 className="text-lg font-bold text-neutral-800 sm:text-xl">
{trip.title}
</h1>
<p className="mt-0.5 flex items-center gap-1.5 text-sm text-neutral-500">
🏔 {trip.mountain}
</p>
</div>
<span
className={`shrink-0 rounded-full px-2.5 py-0.5 text-xs font-bold sm:px-3 sm:py-1 ${
trip.status === "OPEN"
? "bg-primary-100 text-primary-700"
: trip.status === "FULL"
? "bg-amber-100 text-amber-700"
: "bg-neutral-100 text-neutral-500"
}`}
>
{trip.status}
</span>
</div>
</div>
<div className="space-y-5 p-4 sm:space-y-6 sm:p-6">
{/* Info Grid */}
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-3 sm:gap-3 sm:p-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-secondary-100 text-sm sm:h-10 sm:w-10 sm:text-lg">
📍
</span>
<div className="min-w-0">
<p className="text-[10px] font-medium text-neutral-400 sm:text-xs">Lokasi</p>
<p className="truncate text-xs font-semibold text-neutral-800 sm:text-sm">{trip.location}</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-3 sm:gap-3 sm:p-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-secondary-100 text-sm sm:h-10 sm:w-10 sm:text-lg">
📅
</span>
<div className="min-w-0">
<p className="text-[10px] font-medium text-neutral-400 sm:text-xs">Tanggal</p>
<p className="truncate text-xs font-semibold text-neutral-800 sm:text-sm">
{formatDateRange(trip.date, trip.endDate)}
</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-xl bg-primary-50 p-3 sm:gap-3 sm:p-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary-100 text-sm sm:h-10 sm:w-10 sm:text-lg">
💰
</span>
<div className="min-w-0">
<p className="text-[10px] font-medium text-primary-600 sm:text-xs">Harga</p>
<p className="text-base font-bold text-primary-700 sm:text-xl">
{formatRupiah(trip.price)}
</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-3 sm:gap-3 sm:p-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-200 text-sm sm:h-10 sm:w-10 sm:text-lg">
👤
</span>
<div className="min-w-0">
<p className="text-[10px] font-medium text-neutral-400 sm:text-xs">Organizer</p>
<p className="truncate text-xs font-semibold text-neutral-800 sm:text-sm">
{trip.organizer.name}
</p>
</div>
</div>
</div>
{/* Participant Progress */}
<div className="rounded-xl border border-neutral-200 p-3 sm:p-4">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-semibold text-neutral-700 sm:text-sm">
Peserta
</span>
<span className="text-xs font-bold text-neutral-800 sm:text-sm">
{participantCount}{" "}
<span className="font-normal text-neutral-400">
/ {trip.maxParticipants}
</span>
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-neutral-100 sm:h-2.5">
<div
className={`h-full rounded-full transition-all ${
fillPercent >= 100
? "bg-amber-500"
: fillPercent >= 70
? "bg-secondary-500"
: "bg-primary-500"
}`}
style={{ width: `${fillPercent}%` }}
/>
</div>
<p className="mt-1.5 text-[11px] text-neutral-500 sm:text-xs">
{spotsLeft > 0
? `${spotsLeft} slot tersisa — yuk gabung!`
: "Trip sudah penuh"}
</p>
</div>
{/* Description */}
{trip.description && (
<div>
<h2 className="mb-2 text-xs font-bold text-neutral-700 sm:text-sm">
Deskripsi Trip
</h2>
<p className="whitespace-pre-wrap text-xs leading-relaxed text-neutral-600 sm:text-sm">
{trip.description}
</p>
</div>
)}
{/* Action */}
<JoinTripButton
tripId={trip.id}
isLoggedIn={!!session?.user}
isOrganizer={isOrganizer}
isJoined={!!currentParticipation}
isFull={spotsLeft <= 0}
tripStatus={trip.status}
/>
{/* Participants List */}
<div>
<h2 className="mb-3 text-xs font-bold text-neutral-700 sm:text-sm">
Peserta ({participantCount})
</h2>
{participantCount === 0 ? (
<p className="text-xs text-neutral-400 sm:text-sm">
Belum ada peserta. Jadilah yang pertama! 🎒
</p>
) : (
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{activeParticipants.map((p) => (
<div
key={p.id}
className="flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 sm:gap-2 sm:px-3 sm:py-1.5"
>
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-600 text-[9px] font-bold text-white sm:h-6 sm:w-6 sm:text-[10px]">
{p.user.name.charAt(0).toUpperCase()}
</div>
<span className="text-xs font-medium text-neutral-700 sm:text-sm">
{p.user.name}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}