Files
setrip/app/(public)/profile/page.tsx
T
2026-05-21 11:59:02 +07:00

279 lines
10 KiB
TypeScript

import type { Metadata } from "next";
import { redirect } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { profileService } from "@/server/services/profile.service";
import { payoutRepo } from "@/server/repositories/payout.repo";
import { TripCard } from "@/features/trip/components/trip-card";
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
import { ProfileEditor } from "@/features/profile/components/profile-editor";
import { EarningsSection } from "@/features/payout/components/earnings-section";
import { Plus, ChevronRight } from "lucide-react";
export const metadata: Metadata = {
title: "Profil Saya",
robots: { index: false, follow: false },
};
export default async function ProfilePage() {
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect("/login?callbackUrl=/profile");
}
const [data, ownProfile, payouts] = await Promise.all([
profileService.getProfileDashboard(session.user.id),
profileService.getOwnProfile(session.user.id),
payoutRepo.listForOrganizer(session.user.id),
]);
const {
user,
isVerifiedOrganizer,
organizedTrips,
activeJoined,
cancelledJoined,
reviewable,
} = data;
const memberSince = new Intl.DateTimeFormat("id-ID", {
month: "long",
year: "numeric",
}).format(user.createdAt);
return (
<div className="mx-auto max-w-4xl px-4 py-6 sm:py-8">
<div className="mb-6 flex flex-col gap-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:flex-row sm:items-center sm:justify-between sm:p-6">
<div className="flex items-center gap-4">
{user.image ? (
<Image
src={user.image}
alt=""
width={72}
height={72}
className="h-18 w-18 rounded-full object-cover"
/>
) : (
<div className="flex h-18 w-18 shrink-0 items-center justify-center rounded-full bg-primary-600 text-2xl font-bold text-white">
{user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="min-w-0">
<h1 className="text-xl font-bold text-neutral-800 sm:text-2xl">
{user.name}
</h1>
<p className="mt-0.5 truncate text-sm text-neutral-500">
{user.email}
</p>
<p className="mt-1 text-xs text-neutral-400">
Anggota sejak {memberSince}
</p>
<p className="mt-2 text-xs text-neutral-500">
Di sini kamu bisa lihat trip yang kamu buat sebagai{" "}
<span className="font-semibold text-primary-700">organizer</span>,
trip yang kamu{" "}
<span className="font-semibold text-secondary-700">ikuti</span>{" "}
sebagai peserta, dan{" "}
<span className="font-semibold text-amber-700">ulasan</span> untuk
trip yang sudah selesai (lewat halaman trip).
</p>
</div>
</div>
<Link
href="/create-trip"
className="inline-flex shrink-0 items-center justify-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2.5 text-sm font-semibold text-white shadow-md hover:bg-primary-700"
>
<Plus size={16} strokeWidth={2} aria-hidden />
Buat trip
</Link>
</div>
{/* Pendapatan dari peserta (escrow payout) */}
<EarningsSection payouts={payouts} />
{/* Profil sosial publik */}
<div className="mb-6">
<ProfileEditor
userId={user.id}
initial={
ownProfile
? {
bio: ownProfile.bio,
city: ownProfile.city,
interests: ownProfile.interests,
instagram: ownProfile.instagram,
vibe: ownProfile.vibe,
}
: null
}
/>
</div>
{/* Trip selesai — akses ulasan (trip ini tidak muncul di Open Trip) */}
{reviewable.length > 0 && (
<section className="mb-8 rounded-2xl border border-amber-200 bg-amber-50/60 p-4 sm:p-5">
<h2 className="mb-1 text-base font-bold text-amber-900 sm:text-lg">
Trip selesai & ulasan
</h2>
<p className="mb-4 text-xs text-amber-900/80 sm:text-sm">
Trip yang sudah lewat tidak tampil di daftar Open Trip. Buka trip di
bawah ini lalu scroll ke bagian ulasan di halaman detail untuk
memberi atau mengubah rating.
</p>
<ul className="space-y-2">
{reviewable.map((p) => {
const t = p.trip;
const hasReview = t.reviews.length > 0;
return (
<li key={p.id}>
<ProfileTripRow
href={`/trips/${t.id}`}
title={t.title}
destination={t.destination}
date={t.date}
endDate={t.endDate}
rightSlot={
<span
className={`inline-flex items-center gap-0.5 ${
hasReview
? "text-secondary-700"
: "font-bold text-amber-800"
}`}
>
{hasReview ? "Ubah ulasan" : "Beri ulasan"}
<ChevronRight size={14} strokeWidth={2} aria-hidden />
</span>
}
/>
</li>
);
})}
</ul>
</section>
)}
<div className="grid gap-8 lg:grid-cols-2">
<section>
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
Trip yang kamu buat
<span className="ml-2 text-sm font-normal text-neutral-400">
({organizedTrips.length})
</span>
</h2>
{organizedTrips.length === 0 ? (
<p className="rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-6 text-center text-sm text-neutral-500">
Belum ada trip.{" "}
<Link
href="/create-trip"
className="font-semibold text-primary-600"
>
Buat trip pertama
</Link>
</p>
) : (
<div className="space-y-4">
{organizedTrips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
category={trip.category}
vibe={trip.vibe}
destination={trip.destination}
location={trip.location}
date={trip.date}
endDate={trip.endDate}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={`${user.name} (Kamu)`}
status={trip.status}
coverImage={trip.images[0]?.url}
isVerifiedOrganizer={isVerifiedOrganizer}
/>
))}
</div>
)}
</section>
<section>
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
Trip yang kamu ikuti
<span className="ml-2 text-sm font-normal text-neutral-400">
({activeJoined.length})
</span>
</h2>
{activeJoined.length === 0 ? (
<p className="rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-6 text-center text-sm text-neutral-500">
Belum join trip.{" "}
<Link href="/trips" className="font-semibold text-primary-600">
Cari open trip
</Link>
</p>
) : (
<ul className="space-y-2">
{activeJoined.map((p) => {
const t = p.trip;
return (
<li key={p.id}>
<ProfileTripRow
href={`/trips/${t.id}`}
title={t.title}
destination={t.destination}
date={t.date}
endDate={t.endDate}
rightSlot={
<span
className={
p.status === "PENDING"
? "font-medium text-amber-700"
: "text-neutral-400"
}
>
{p.status === "CONFIRMED"
? "Terkonfirmasi"
: p.status === "PENDING"
? "Menunggu organizer"
: p.status}
</span>
}
/>
</li>
);
})}
</ul>
)}
{cancelledJoined.length > 0 && (
<div className="mt-6">
<h3 className="mb-2 text-sm font-semibold text-neutral-500">
Riwayat batal ({cancelledJoined.length})
</h3>
<ul className="space-y-2 opacity-80">
{cancelledJoined.map((p) => {
const t = p.trip;
return (
<li key={p.id}>
<ProfileTripRow
href={`/trips/${t.id}`}
title={t.title}
destination={t.destination}
date={t.date}
endDate={t.endDate}
rightSlot={
<span className="text-red-500/90">Dibatalkan</span>
}
/>
</li>
);
})}
</ul>
</div>
)}
</section>
</div>
</div>
);
}