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

216 lines
7.9 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, formatDate } from "@/lib/utils";
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
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-8">
{/* Breadcrumb */}
<div className="mb-4 flex items-center gap-2 text-sm text-neutral-500">
<Link href="/trips" className="hover:text-primary-600">
Open Trip
</Link>
<span>/</span>
<span className="text-neutral-700">{trip.mountain}</span>
</div>
<div className="overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm">
{/* Header — dark theme */}
<div className="relative overflow-hidden bg-neutral-800 px-6 py-8">
<div className="absolute inset-0 bg-linear-to-br from-primary-900/40 to-secondary-900/30" />
<div className="relative">
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-white">{trip.title}</h1>
<p className="mt-1 flex items-center gap-1.5 text-neutral-300">
<span>🏔</span> {trip.mountain}
</p>
</div>
<span
className={`rounded-full px-3 py-1 text-xs font-bold ${
trip.status === "OPEN"
? "bg-primary-500/20 text-primary-300 ring-1 ring-primary-400/30"
: trip.status === "FULL"
? "bg-amber-500/20 text-amber-300 ring-1 ring-amber-400/30"
: "bg-neutral-500/20 text-neutral-400 ring-1 ring-neutral-500/30"
}`}
>
{trip.status}
</span>
</div>
</div>
</div>
<div className="p-6 space-y-6">
{/* Info Grid */}
<div className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-secondary-100 text-lg">
📍
</span>
<div>
<p className="text-xs font-medium text-neutral-400">Lokasi</p>
<p className="font-semibold text-neutral-800">{trip.location}</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-secondary-100 text-lg">
📅
</span>
<div>
<p className="text-xs font-medium text-neutral-400">Tanggal</p>
<p className="font-semibold text-neutral-800">
{formatDate(trip.date)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-100 text-lg">
💰
</span>
<div>
<p className="text-xs font-medium text-primary-600">Harga</p>
<p className="text-xl font-bold text-primary-700">
{formatRupiah(trip.price)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-200 text-lg">
👤
</span>
<div>
<p className="text-xs font-medium text-neutral-400">
Organizer
</p>
<p className="font-semibold text-neutral-800">
{trip.organizer.name}
</p>
</div>
</div>
</div>
{/* Participant Progress */}
<div className="rounded-xl border border-neutral-200 p-4">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-semibold text-neutral-700">
Peserta
</span>
<span className="text-sm font-bold text-neutral-800">
{participantCount}{" "}
<span className="font-normal text-neutral-400">
/ {trip.maxParticipants}
</span>
</span>
</div>
<div className="h-2.5 overflow-hidden rounded-full bg-neutral-100">
<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-xs text-neutral-500">
{spotsLeft > 0
? `${spotsLeft} slot tersisa — yuk gabung!`
: "Trip sudah penuh"}
</p>
</div>
{/* Description */}
{trip.description && (
<div>
<h2 className="mb-2 text-sm font-bold text-neutral-700">
Deskripsi Trip
</h2>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-neutral-600">
{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-sm font-bold text-neutral-700">
Peserta ({participantCount})
</h2>
{participantCount === 0 ? (
<p className="text-sm text-neutral-400">
Belum ada peserta. Jadilah yang pertama! 🎒
</p>
) : (
<div className="flex flex-wrap gap-2">
{activeParticipants.map((p) => (
<div
key={p.id}
className="flex items-center gap-2 rounded-full bg-neutral-100 px-3 py-1.5"
>
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary-600 text-[10px] font-bold text-white">
{p.user.name.charAt(0).toUpperCase()}
</div>
<span className="text-sm font-medium text-neutral-700">
{p.user.name}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}