add user profile, profile vibe and trip vibe and social signal
This commit is contained in:
+79
-19
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.service";
|
||||
import { trustService } from "@/server/services/trust.service";
|
||||
@@ -15,6 +16,8 @@ import { TripProgramBlock } from "@/features/trip/components/trip-program-block"
|
||||
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
||||
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
||||
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||
import { categoryMeta } from "@/lib/activity-category";
|
||||
import { vibeMeta } from "@/lib/vibe";
|
||||
import {
|
||||
isPastTripLastDayForReview,
|
||||
isTripDepartureDayPast,
|
||||
@@ -127,6 +130,8 @@ export default async function TripDetailPage({
|
||||
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
||||
);
|
||||
|
||||
const catMeta = categoryMeta(trip.category);
|
||||
|
||||
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
||||
const eventStatus =
|
||||
trip.status === "OPEN"
|
||||
@@ -240,8 +245,21 @@ export default async function TripDetailPage({
|
||||
<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.destination}
|
||||
<p className="mt-0.5 flex flex-wrap items-center gap-1.5 text-sm text-neutral-500">
|
||||
<span aria-hidden>{catMeta.icon}</span>
|
||||
<span className="rounded-full bg-neutral-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
{catMeta.label}
|
||||
</span>
|
||||
{trip.vibe && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 rounded-full bg-secondary-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-secondary-700"
|
||||
title={vibeMeta(trip.vibe).description}
|
||||
>
|
||||
<span aria-hidden>{vibeMeta(trip.vibe).icon}</span>
|
||||
<span>{vibeMeta(trip.vibe).label}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{trip.destination}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
@@ -448,9 +466,12 @@ export default async function TripDetailPage({
|
||||
|
||||
{/* Peserta yang sudah disetujui organizer (publik) */}
|
||||
<div>
|
||||
<h2 className="mb-3 text-xs font-bold text-neutral-700 sm:text-sm">
|
||||
<h2 className="mb-1 text-xs font-bold text-neutral-700 sm:text-sm">
|
||||
Peserta terkonfirmasi ({confirmedCount})
|
||||
</h2>
|
||||
<p className="mb-3 text-[11px] text-neutral-500 sm:text-xs">
|
||||
Kenalan dulu sebelum berangkat — klik kartu untuk lihat profil.
|
||||
</p>
|
||||
{confirmedCount === 0 ? (
|
||||
<p className="text-xs text-neutral-400 sm:text-sm">
|
||||
Belum ada peserta yang dikonfirmasi.{" "}
|
||||
@@ -459,22 +480,61 @@ export default async function TripDetailPage({
|
||||
: "Jadilah yang pertama mendaftar! 🎒"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{confirmedParticipants.map((p) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
href={`/u/${p.user.id}`}
|
||||
className="flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 transition-colors hover:bg-primary-100 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>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{confirmedParticipants.map((p) => {
|
||||
const interests = p.user.profile?.interests ?? [];
|
||||
const city = p.user.profile?.city;
|
||||
return (
|
||||
<li key={p.id}>
|
||||
<Link
|
||||
href={`/u/${p.user.id}`}
|
||||
className="flex items-start gap-3 rounded-xl border border-neutral-200 bg-white px-3 py-2.5 transition-colors hover:border-primary-300 hover:bg-primary-50/30"
|
||||
>
|
||||
{p.user.image ? (
|
||||
<Image
|
||||
src={p.user.image}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary-600 text-sm font-bold text-white">
|
||||
{p.user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold text-neutral-800">
|
||||
{p.user.name}
|
||||
</p>
|
||||
{city && (
|
||||
<p className="truncate text-[11px] text-neutral-500 sm:text-xs">
|
||||
📍 {city}
|
||||
</p>
|
||||
)}
|
||||
{interests.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{interests.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-secondary-50 px-1.5 py-0.5 text-[10px] font-medium text-secondary-700"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{interests.length > 3 && (
|
||||
<span className="text-[10px] text-neutral-400">
|
||||
+{interests.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+33
-2
@@ -1,11 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.service";
|
||||
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||
import { TripCard } from "@/features/trip/components/trip-card";
|
||||
import { TripFilter } from "@/features/trip/components/trip-filter";
|
||||
import { siteConfig } from "@/lib/site";
|
||||
import { categoryLabel, isActivityCategory } from "@/lib/activity-category";
|
||||
import { isVibe } from "@/lib/vibe";
|
||||
import type { GroupSize } from "@/server/repositories/trip.repo";
|
||||
|
||||
const GROUP_SIZES: GroupSize[] = ["SMALL", "MEDIUM", "LARGE"];
|
||||
function isGroupSize(value: unknown): value is GroupSize {
|
||||
return typeof value === "string" && (GROUP_SIZES as string[]).includes(value);
|
||||
}
|
||||
|
||||
interface TripsPageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -13,6 +23,8 @@ interface TripsPageProps {
|
||||
from?: string;
|
||||
to?: string;
|
||||
category?: string;
|
||||
vibe?: string;
|
||||
groupSize?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -44,19 +56,30 @@ export async function generateMetadata({
|
||||
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||
const params = await searchParams;
|
||||
const category = isActivityCategory(params.category) ? params.category : undefined;
|
||||
const hasFilters = Boolean(params.q || params.from || params.to || category);
|
||||
const vibe = isVibe(params.vibe) ? params.vibe : undefined;
|
||||
const groupSize = isGroupSize(params.groupSize) ? params.groupSize : undefined;
|
||||
const hasFilters = Boolean(
|
||||
params.q || params.from || params.to || category || vibe || groupSize
|
||||
);
|
||||
const filters = {
|
||||
q: params.q,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
category,
|
||||
vibe,
|
||||
groupSize,
|
||||
};
|
||||
|
||||
const [trips, allTrips] = await Promise.all([
|
||||
const session = await getServerSession(authOptions);
|
||||
const [trips, allTrips, viewerProfile] = await Promise.all([
|
||||
tripService.getOpenTrips(filters),
|
||||
hasFilters ? tripService.getOpenTrips() : null,
|
||||
session?.user?.id
|
||||
? profileRepo.findByUserId(session.user.id)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const totalCount = hasFilters ? allTrips!.length : trips.length;
|
||||
const viewerInterests = viewerProfile?.interests ?? [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 sm:py-8">
|
||||
@@ -120,6 +143,7 @@ export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
@@ -133,6 +157,13 @@ export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||
isVerifiedOrganizer={
|
||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||
}
|
||||
participants={trip.participants.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.user.name,
|
||||
image: p.user.image,
|
||||
interests: p.user.profile?.interests ?? [],
|
||||
}))}
|
||||
viewerInterests={viewerInterests}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user