Files
setrip/app/page.tsx
T
2026-05-07 18:11:59 +07:00

326 lines
13 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 type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { tripService } from "@/server/services/trip.service";
import { TripCard } from "@/features/trip/components/trip-card";
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
export const metadata: Metadata = {
title: "Cari Teman Trip & Aktivitas — Pergi Bareng, Bukan Sendiri",
description: `${siteConfig.slogan} ${siteConfig.description}`,
alternates: { canonical: "/" },
openGraph: {
title: `${siteConfig.name} — Cari Teman Trip & Aktivitas, Gabung Bareng`,
description: siteConfig.slogan,
url: "/",
},
};
export default async function HomePage() {
const trips = await tripService.getOpenTrips();
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const upcomingTrips = trips
.filter((t) => new Date(t.date) <= nextWeek)
.slice(0, 3);
const upcomingIds = new Set(upcomingTrips.map((t) => t.id));
const latestTrips = trips
.filter((t) => !upcomingIds.has(t.id))
.slice(0, 6);
const shownIds = new Set([...upcomingIds, ...latestTrips.map((t) => t.id)]);
const budgetTrips = trips
.filter((t) => !shownIds.has(t.id) && t.price <= 300000)
.slice(0, 3);
const orgJsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
name: siteConfig.name,
url: siteUrl,
logo: absoluteUrl("/images/SeTrip.png"),
slogan: siteConfig.slogan,
description: siteConfig.description,
},
{
"@type": "WebSite",
"@id": `${siteUrl}/#website`,
url: siteUrl,
name: siteConfig.name,
description: siteConfig.description,
publisher: { "@id": `${siteUrl}/#organization` },
inLanguage: "id-ID",
potentialAction: {
"@type": "SearchAction",
target: `${siteUrl}/trips?q={search_term_string}`,
"query-input": "required name=search_term_string",
},
},
],
};
return (
<div className="relative min-h-screen bg-neutral-50">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
/>
{/* ========== HERO ========== */}
<section className="relative overflow-hidden bg-neutral-900">
{/* Logo background full */}
<Image
src="/images/SeTrip.png"
alt=""
fill
className="object-cover opacity-10 brightness-150"
priority
/>
<div className="absolute inset-0 bg-linear-to-br from-primary-900/85 via-neutral-900/75 to-secondary-900/80" />
<div className="relative mx-auto max-w-4xl px-4 pb-10 pt-8 text-center sm:pb-14 sm:pt-12 lg:pb-16 lg:pt-14">
{/* Brand badge */}
<div className="mb-4 inline-flex items-center gap-1.5 rounded-full border border-primary-400/30 bg-primary-600/20 px-3 py-1 sm:mb-6 sm:gap-2 sm:px-4 sm:py-1.5">
<span className="text-xs sm:text-sm">🤝</span>
<span className="text-xs font-medium text-primary-300 sm:text-sm">
Cari teman trip & aktivitas
</span>
</div>
<h1 className="mb-3 text-3xl font-extrabold leading-tight tracking-tight text-white sm:mb-4 sm:text-4xl lg:text-5xl">
Pergi bareng,{" "}
<span className="text-primary-400">bukan sendiri</span>
</h1>
<p className="mx-auto mb-2 max-w-sm text-base font-medium text-neutral-300 sm:mb-3 sm:max-w-lg sm:text-lg">
Lagi pengen jalan tapi gak punya teman?{" "}
<span className="text-primary-400">SeTrip</span> tempatnya.
</p>
<p className="mx-auto mb-6 max-w-xs text-sm text-neutral-400 sm:mb-8 sm:max-w-md sm:text-base">
Gabung open trip hiking, camping, snorkeling, sampai city trip.
Ketemu orang baru, dari stranger jadi travel buddies. Grup kecil,
organizer terverifikasi.
</p>
<Link
href="/trips"
className="inline-block rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 transition-all hover:bg-primary-500 hover:scale-105 active:scale-95 sm:px-8 sm:py-3 sm:text-base"
>
Cari Teman Trip
</Link>
{/* Stats */}
<div className="mt-8 flex justify-center gap-6 sm:mt-10 sm:gap-10 lg:gap-12">
<div>
<p className="text-xl font-bold text-primary-400 sm:text-2xl">
{trips.length}
</p>
<p className="text-[11px] text-neutral-400 sm:text-xs">Trip Tersedia</p>
</div>
<div className="h-8 w-px bg-neutral-700 sm:h-10" />
<div>
<p className="text-xl font-bold text-white sm:text-2xl">100%</p>
<p className="text-[11px] text-neutral-400 sm:text-xs">Seru</p>
</div>
</div>
</div>
</section>
{/* ========== CONTENT ========== */}
<div className="mx-auto max-w-6xl px-4 py-6 space-y-8 sm:py-8 sm:space-y-10 lg:py-10 lg:space-y-12">
{/* Trip Terdekat */}
{upcomingTrips.length > 0 && (
<section>
<div className="mb-4 flex items-center gap-3 sm:mb-5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100 text-base sm:h-9 sm:w-9 sm:text-lg">
🔥
</div>
<div>
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
Trip Terdekat
</h2>
<p className="text-[11px] text-neutral-500 sm:text-xs">
Berangkat dalam 7 hari ke depan
</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{upcomingTrips.slice(0, 3).map((trip, i) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
endDate={trip.endDate}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
coverImage={trip.images[0]?.url}
isVerifiedOrganizer={
trip.organizer.organizerVerification?.status === "APPROVED"
}
priority={i === 0}
/>
))}
</div>
</section>
)}
{/* Open Trip */}
<section>
<div className="mb-4 flex items-center justify-between sm:mb-5">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-secondary-100 text-base sm:h-9 sm:w-9 sm:text-lg">
🏔
</div>
<div>
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
Open Trip
</h2>
<p className="hidden text-xs text-neutral-500 sm:block">
Pilih trip, ketemu teman baru
</p>
</div>
</div>
<Link
href="/trips"
className="rounded-lg bg-secondary-50 px-2.5 py-1 text-xs font-medium text-secondary-600 hover:bg-secondary-100 sm:px-3 sm:py-1.5 sm:text-sm"
>
Lihat semua
</Link>
</div>
{latestTrips.length === 0 ? (
<div className="rounded-2xl border-2 border-dashed border-neutral-200 bg-white p-8 text-center sm:p-14">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary-50 text-2xl sm:h-16 sm:w-16 sm:text-3xl">
🏕
</div>
<p className="mb-1 text-base font-bold text-neutral-800 sm:text-lg">
Belum ada trip tersedia
</p>
<p className="mb-5 text-sm text-neutral-500 sm:mb-6">
Jadilah yang pertama buat open trip di sini!
</p>
<Link
href="/create-trip"
className="inline-block rounded-xl bg-primary-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 hover:bg-primary-700"
>
+ Buat Trip Baru
</Link>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{latestTrips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
endDate={trip.endDate}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
coverImage={trip.images[0]?.url}
isVerifiedOrganizer={
trip.organizer.organizerVerification?.status === "APPROVED"
}
/>
))}
</div>
)}
</section>
{/* Budget Friendly */}
{budgetTrips.length > 0 && (
<section>
<div className="mb-4 flex items-center gap-3 sm:mb-5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100 text-base sm:h-9 sm:w-9 sm:text-lg">
💸
</div>
<div>
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
Budget Friendly
</h2>
<p className="text-[11px] text-neutral-500 sm:text-xs">
Trip di bawah Rp 300.000
</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{budgetTrips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
endDate={trip.endDate}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
coverImage={trip.images[0]?.url}
isVerifiedOrganizer={
trip.organizer.organizerVerification?.status === "APPROVED"
}
/>
))}
</div>
</section>
)}
{/* CTA Bottom */}
<section className="overflow-hidden rounded-2xl bg-neutral-800 p-6 text-center sm:p-8 lg:p-12">
<h2 className="mb-2 text-xl font-bold text-white sm:text-2xl">
Dari stranger, jadi travel buddies.
</h2>
<p className="mx-auto mb-5 max-w-xs text-sm text-neutral-400 sm:mb-6 sm:max-w-sm sm:text-base">
Buat trip dan kumpulin grup sendiri, atau gabung trip yang sudah
jalan. Kenalan baru menunggu di puncak.
</p>
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/create-trip"
className="rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 hover:bg-primary-500"
>
Buat Trip
</Link>
<Link
href="/trips"
className="rounded-xl border border-neutral-600 px-6 py-2.5 text-sm font-semibold text-neutral-300 hover:border-neutral-500 hover:text-white"
>
Cari Teman Trip
</Link>
</div>
</section>
</div>
{/* ========== FAB ========== */}
<Link
href="/create-trip"
className="fixed bottom-4 right-4 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-primary-600 text-xl font-bold text-white shadow-xl shadow-primary-600/30 transition-all hover:scale-110 hover:bg-primary-500 active:scale-95 sm:bottom-6 sm:right-6 sm:h-14 sm:w-14 sm:text-2xl"
title="Buat Trip"
>
+
</Link>
</div>
);
}