Files
setrip/features/trip/components/trip-card.tsx
T

222 lines
8.1 KiB
TypeScript

import Image from "next/image";
import Link from "next/link";
import { formatRupiah } from "@/lib/utils";
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
import { categoryMeta } from "@/lib/activity-category";
import { vibeMeta } from "@/lib/vibe";
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
interface TripCardParticipant {
id: string;
name: string;
image: string | null;
interests: string[];
}
interface TripCardProps {
id: string;
title: string;
category: ActivityCategory;
vibe?: Vibe | null;
destination: string;
location: string;
date: Date | string;
endDate?: Date | string | null;
price: number;
maxParticipants: number;
participantCount: number;
organizerName: string;
status: string;
coverImage?: string | null;
priority?: boolean;
isVerifiedOrganizer?: boolean;
/** Daftar peserta CONFIRMED (subset, untuk preview avatar). Optional. */
participants?: TripCardParticipant[];
/** Interests user yang sedang melihat — untuk hitung overlap. Optional. */
viewerInterests?: string[];
}
export function TripCard({
id,
title,
category,
vibe,
destination,
location,
date,
endDate,
price,
maxParticipants,
participantCount,
organizerName,
status,
coverImage,
priority,
isVerifiedOrganizer,
participants,
viewerInterests,
}: TripCardProps) {
const spotsLeft = maxParticipants - participantCount;
const isSmallGroup = maxParticipants <= 10;
const meta = categoryMeta(category);
const vMeta = vibe ? vibeMeta(vibe) : null;
const previewParticipants = participants?.slice(0, 3) ?? [];
const moreCount =
participants && participants.length > 3 ? participants.length - 3 : 0;
let overlapCount = 0;
if (viewerInterests && viewerInterests.length > 0 && participants) {
const viewerSet = new Set(viewerInterests.map((i) => i.toLowerCase()));
overlapCount = participants.filter((p) =>
p.interests.some((tag) => viewerSet.has(tag.toLowerCase()))
).length;
}
return (
<Link href={`/trips/${id}`} className="group block">
<div className="overflow-hidden rounded-2xl border border-neutral-200 bg-white transition-all group-hover:-translate-y-0.5 group-hover:shadow-lg group-hover:shadow-neutral-200/60">
{/* Cover Image */}
<div className="relative h-40 bg-neutral-800">
{coverImage ? (
<Image
src={coverImage}
alt={title}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
priority={priority}
/>
) : (
<div className="flex h-full items-center justify-center bg-linear-to-br from-primary-800 to-secondary-900">
<span className="text-4xl">{meta.icon}</span>
</div>
)}
<div className="absolute left-3 top-3 flex flex-wrap gap-1">
<span
className="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-semibold text-neutral-700 shadow-sm backdrop-blur-sm"
title={`Kategori: ${meta.label}`}
>
<span>{meta.icon}</span>
<span>{meta.label}</span>
</span>
{vMeta && (
<span
className="inline-flex items-center gap-1 rounded-full bg-secondary-600/90 px-2 py-0.5 text-[11px] font-semibold text-white shadow-sm backdrop-blur-sm"
title={`Vibe: ${vMeta.label}${vMeta.description}`}
>
<span aria-hidden>{vMeta.icon}</span>
<span>{vMeta.label}</span>
</span>
)}
</div>
<span
className={`absolute right-3 top-3 rounded-full px-2.5 py-0.5 text-xs font-bold backdrop-blur-sm ${
status === "OPEN"
? "bg-primary-600/80 text-white"
: status === "FULL"
? "bg-amber-500/80 text-white"
: "bg-neutral-600/80 text-neutral-200"
}`}
>
{status}
</span>
</div>
{/* Content */}
<div className="p-4">
<h3 className="font-bold text-neutral-800 group-hover:text-primary-700">
{title}
</h3>
<p className="mt-0.5 text-sm text-neutral-500">{destination}</p>
<div className="mt-3 space-y-1 text-sm text-neutral-600">
<div className="flex items-center gap-1.5">
<span className="text-xs text-secondary-500">📍</span> {location}
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs text-secondary-500">📅</span>{" "}
{formatTripCalendarDateRangeLong(date, endDate)}
</div>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs text-secondary-500">👤</span>{" "}
<span className="truncate">{organizerName}</span>
{isVerifiedOrganizer && (
<span
className="inline-flex items-center gap-0.5 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
title="Identitas organizer telah diverifikasi (KTP & rekening)"
>
Verified
</span>
)}
{isSmallGroup && (
<span
className="inline-flex items-center rounded-full bg-secondary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-secondary-800"
title="Grup kecil — pengalaman lebih akrab"
>
Small group
</span>
)}
</div>
</div>
{(previewParticipants.length > 0 || overlapCount > 0) && (
<div className="mt-3 flex items-center gap-2 border-t border-neutral-100 pt-3">
{previewParticipants.length > 0 && (
<div className="flex -space-x-2">
{previewParticipants.map((p) =>
p.image ? (
<Image
key={p.id}
src={p.image}
alt=""
width={24}
height={24}
className="h-6 w-6 rounded-full border-2 border-white object-cover"
/>
) : (
<div
key={p.id}
className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-primary-600 text-[10px] font-bold text-white"
title={p.name}
>
{p.name.charAt(0).toUpperCase()}
</div>
)
)}
{moreCount > 0 && (
<div className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-neutral-200 text-[10px] font-bold text-neutral-600">
+{moreCount}
</div>
)}
</div>
)}
{overlapCount > 0 && (
<span
className="rounded-full bg-secondary-50 px-2 py-0.5 text-[11px] font-semibold text-secondary-700"
title="Peserta dengan minimal 1 minat sama dengan kamu"
>
{overlapCount} peserta sama minat
</span>
)}
</div>
)}
<div className="mt-3 flex items-center justify-between border-t border-neutral-100 pt-3">
<span className="text-lg font-bold text-primary-600">
{formatRupiah(price)}
</span>
<span
className={`text-xs font-semibold ${
spotsLeft > 0 ? "text-secondary-600" : "text-amber-600"
}`}
>
{spotsLeft > 0 ? `${spotsLeft} slot tersisa` : "Penuh"}
</span>
</div>
</div>
</div>
</Link>
);
}