feat(seo): add metadata, sitemap, robots, JSON-LD, dynamic OG image
- Centralize brand/keyword config in lib/site.ts (slogan, 22 keywords). - Root layout: metadataBase, title template, OG/Twitter defaults, robots rules. - Per-page metadata: home, trips list (filter-aware), trip detail, profile (noindex). - Layout wrappers add metadata to client-component pages: login, register, create-trip. - Trip detail: generateMetadata + JSON-LD Event + BreadcrumbList (price, availability, rating). - Home page: JSON-LD Organization + WebSite + SearchAction (sitelink search). - app/sitemap.ts: dynamic sitemap pulling OPEN/FULL trips from Prisma. - app/robots.ts: disallow /api/, /profile, /create-trip; references sitemap. - app/trips/[id]/opengraph-image.tsx: dynamic 1200x630 OG image per trip with cover photo, title, mountain, date, price, brand badge. - Seeder: switch trip images from local SVG placeholders to real Unsplash CDN URLs. - Drop 18 obsolete seed SVGs from public/images/seed/. New env: NEXT_PUBLIC_SITE_URL (defaults to localhost:3000) — set to prod domain on deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Buat Open Trip",
|
||||||
|
description:
|
||||||
|
"Buat open trip pendakian gunung di SeTrip. Atur itinerary, harga, dan ajak pendaki lain ikut serta.",
|
||||||
|
alternates: { canonical: "/create-trip" },
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CreateTripLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { SessionProvider } from "@/components/providers/session-provider";
|
import { SessionProvider } from "@/components/providers/session-provider";
|
||||||
import { Navbar } from "@/components/shared/navbar";
|
import { Navbar } from "@/components/shared/navbar";
|
||||||
|
import { siteConfig, siteUrl } from "@/lib/site";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -15,9 +16,50 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SeTrip",
|
metadataBase: new URL(siteUrl),
|
||||||
description:
|
title: {
|
||||||
"Cari open trip pendakian gunung, gabung bareng, nikmati petualangan ke gunung-gunung Jawa Barat.",
|
default: `${siteConfig.name} — Open Trip Pendakian Gunung`,
|
||||||
|
template: `%s · ${siteConfig.name}`,
|
||||||
|
},
|
||||||
|
description: siteConfig.description,
|
||||||
|
applicationName: siteConfig.name,
|
||||||
|
keywords: [...siteConfig.keywords],
|
||||||
|
authors: [{ name: siteConfig.name }],
|
||||||
|
creator: siteConfig.name,
|
||||||
|
publisher: siteConfig.name,
|
||||||
|
alternates: { canonical: "/" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "id_ID",
|
||||||
|
url: "/",
|
||||||
|
siteName: siteConfig.name,
|
||||||
|
title: `${siteConfig.name} — Open Trip Pendakian Gunung`,
|
||||||
|
description: siteConfig.description,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/images/SeTrip.png",
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: `${siteConfig.name} — ${siteConfig.slogan}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: `${siteConfig.name} — Open Trip Pendakian Gunung`,
|
||||||
|
description: siteConfig.description,
|
||||||
|
images: ["/images/SeTrip.png"],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/SeTrip.ico",
|
icon: "/SeTrip.ico",
|
||||||
apple: "/images/SeTrip.png",
|
apple: "/images/SeTrip.png",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Masuk",
|
||||||
|
description:
|
||||||
|
"Masuk ke akun SeTrip untuk gabung open trip pendakian dan kelola perjalananmu.",
|
||||||
|
alternates: { canonical: "/login" },
|
||||||
|
robots: { index: false, follow: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
|
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Cari Open Trip Pendakian Gunung Bareng",
|
||||||
|
description: `${siteConfig.slogan} ${siteConfig.description}`,
|
||||||
|
alternates: { canonical: "/" },
|
||||||
|
openGraph: {
|
||||||
|
title: `${siteConfig.name} — Open Trip Pendakian Gunung Bareng`,
|
||||||
|
description: siteConfig.slogan,
|
||||||
|
url: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const trips = await tripService.getOpenTrips();
|
const trips = await tripService.getOpenTrips();
|
||||||
@@ -25,8 +38,41 @@ export default async function HomePage() {
|
|||||||
.filter((t) => !shownIds.has(t.id) && t.price <= 300000)
|
.filter((t) => !shownIds.has(t.id) && t.price <= 300000)
|
||||||
.slice(0, 3);
|
.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 (
|
return (
|
||||||
<div className="relative min-h-screen bg-neutral-50">
|
<div className="relative min-h-screen bg-neutral-50">
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||||
|
/>
|
||||||
{/* ========== HERO ========== */}
|
{/* ========== HERO ========== */}
|
||||||
<section className="relative overflow-hidden bg-neutral-900">
|
<section className="relative overflow-hidden bg-neutral-900">
|
||||||
{/* Logo background full */}
|
{/* Logo background full */}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -7,6 +8,11 @@ import { profileService } from "@/server/services/profile.service";
|
|||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Profil Saya",
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
export default async function ProfilePage() {
|
export default async function ProfilePage() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Daftar Akun",
|
||||||
|
description:
|
||||||
|
"Buat akun SeTrip gratis. Cari open trip pendakian gunung, gabung bareng, dan mulai petualanganmu.",
|
||||||
|
alternates: { canonical: "/register" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RegisterLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { absoluteUrl, siteUrl } from "@/lib/site";
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: "*",
|
||||||
|
allow: "/",
|
||||||
|
disallow: ["/api/", "/profile", "/create-trip"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: absoluteUrl("/sitemap.xml"),
|
||||||
|
host: siteUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { absoluteUrl } from "@/lib/site";
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const trips = await prisma.trip.findMany({
|
||||||
|
where: { status: { in: ["OPEN", "FULL"] } },
|
||||||
|
select: { id: true, updatedAt: true },
|
||||||
|
orderBy: { updatedAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const staticEntries: MetadataRoute.Sitemap = [
|
||||||
|
{
|
||||||
|
url: absoluteUrl("/"),
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: "daily",
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: absoluteUrl("/trips"),
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: "hourly",
|
||||||
|
priority: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: absoluteUrl("/register"),
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: "yearly",
|
||||||
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tripEntries: MetadataRoute.Sitemap = trips.map((t) => ({
|
||||||
|
url: absoluteUrl(`/trips/${t.id}`),
|
||||||
|
lastModified: t.updatedAt,
|
||||||
|
changeFrequency: "daily",
|
||||||
|
priority: 0.8,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...staticEntries, ...tripEntries];
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
import { tripService } from "@/server/services/trip.service";
|
||||||
|
import { formatRupiah } from "@/lib/utils";
|
||||||
|
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||||
|
import { siteConfig } from "@/lib/site";
|
||||||
|
|
||||||
|
export const alt = `${siteConfig.name} — Open Trip Pendakian`;
|
||||||
|
export const size = { width: 1200, height: 630 };
|
||||||
|
export const contentType = "image/png";
|
||||||
|
|
||||||
|
export default async function TripOgImage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
let trip;
|
||||||
|
try {
|
||||||
|
trip = await tripService.getTripById(id);
|
||||||
|
} catch {
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, #14532d 0%, #16a34a 60%, #0c4a6e 100%)",
|
||||||
|
color: "white",
|
||||||
|
fontSize: 96,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: -2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{siteConfig.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ ...size }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cover = trip.images[0]?.url;
|
||||||
|
const dateLabel = formatTripCalendarDateRangeLong(trip.date, trip.endDate);
|
||||||
|
const price = formatRupiah(trip.price);
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
position: "relative",
|
||||||
|
background: "#0a0a0a",
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cover && (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={cover}
|
||||||
|
alt=""
|
||||||
|
width={1200}
|
||||||
|
height={630}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
filter: "brightness(0.45) saturate(1.05)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(20,83,45,0.88) 0%, rgba(10,10,10,0.5) 55%, rgba(12,74,110,0.85) 100%)",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: 64,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Top: brand badge */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
background: "rgba(22,163,74,0.25)",
|
||||||
|
border: "1px solid rgba(74,222,128,0.45)",
|
||||||
|
borderRadius: 999,
|
||||||
|
padding: "10px 22px",
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#86efac",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 28 }}>🏔️</span>
|
||||||
|
<span>Open Trip Pendakian</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Middle: title + mountain */}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: trip.title.length > 40 ? 64 : 76,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: -2,
|
||||||
|
lineHeight: 1.05,
|
||||||
|
display: "flex",
|
||||||
|
maxWidth: 1050,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trip.title}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: "#bbf7d0",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>📍</span>
|
||||||
|
<span>
|
||||||
|
{trip.mountain} · {trip.location}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom: date / price / brand */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 32,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
fontSize: 26,
|
||||||
|
color: "#e0f2fe",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 30 }}>📅</span>
|
||||||
|
<span>{dateLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
gap: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 22, color: "#86efac" }}>Mulai</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 56,
|
||||||
|
fontWeight: 800,
|
||||||
|
color: "#4ade80",
|
||||||
|
letterSpacing: -1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{price}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 22, color: "#86efac" }}>/ orang</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: -2,
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Se</span>
|
||||||
|
<span style={{ color: "#4ade80" }}>Trip</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 18, color: "#a3a3a3" }}>
|
||||||
|
{siteConfig.slogan}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ ...size }
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -6,6 +7,7 @@ import { tripService } from "@/server/services/trip.service";
|
|||||||
import { trustService } from "@/server/services/trust.service";
|
import { trustService } from "@/server/services/trust.service";
|
||||||
import { formatRupiah } from "@/lib/utils";
|
import { formatRupiah } from "@/lib/utils";
|
||||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||||
|
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
||||||
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
||||||
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
|
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
|
||||||
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
|
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
|
||||||
@@ -18,6 +20,47 @@ import {
|
|||||||
isTripDepartureDayPast,
|
isTripDepartureDayPast,
|
||||||
} from "@/lib/trip-dates";
|
} from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { id } = await params;
|
||||||
|
let trip;
|
||||||
|
try {
|
||||||
|
trip = await tripService.getTripById(id);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: "Trip tidak ditemukan",
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = `${trip.title} — ${trip.mountain}`;
|
||||||
|
const fallbackDescription = `Open trip ${trip.mountain} di ${trip.location}, ${formatTripCalendarDateRangeLong(trip.date, trip.endDate)}. Harga ${formatRupiah(trip.price)}/orang, max ${trip.maxParticipants} peserta. Gabung di ${siteConfig.name}.`;
|
||||||
|
const description =
|
||||||
|
trip.description?.replace(/\s+/g, " ").trim().slice(0, 160) ||
|
||||||
|
fallbackDescription;
|
||||||
|
|
||||||
|
// OG/Twitter image otomatis di-inject dari `opengraph-image.tsx` colocated.
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/trips/${id}` },
|
||||||
|
openGraph: {
|
||||||
|
type: "article",
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url: `/trips/${id}`,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function TripDetailPage({
|
export default async function TripDetailPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
@@ -84,8 +127,99 @@ export default async function TripDetailPage({
|
|||||||
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
||||||
|
const eventStatus =
|
||||||
|
trip.status === "OPEN"
|
||||||
|
? "https://schema.org/EventScheduled"
|
||||||
|
: trip.status === "CLOSED"
|
||||||
|
? "https://schema.org/EventCancelled"
|
||||||
|
: "https://schema.org/EventScheduled";
|
||||||
|
const offerAvailability =
|
||||||
|
trip.status === "OPEN"
|
||||||
|
? "https://schema.org/InStock"
|
||||||
|
: trip.status === "FULL"
|
||||||
|
? "https://schema.org/SoldOut"
|
||||||
|
: "https://schema.org/Discontinued";
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@type": "Event",
|
||||||
|
"@id": `${tripUrl}#event`,
|
||||||
|
name: trip.title,
|
||||||
|
description: trip.description ?? undefined,
|
||||||
|
startDate: trip.date.toISOString(),
|
||||||
|
endDate: (trip.endDate ?? trip.date).toISOString(),
|
||||||
|
eventStatus,
|
||||||
|
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
|
||||||
|
location: {
|
||||||
|
"@type": "Place",
|
||||||
|
name: trip.mountain,
|
||||||
|
address: {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
addressLocality: trip.location,
|
||||||
|
addressCountry: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
image: trip.images.length
|
||||||
|
? trip.images.map((i) => i.url)
|
||||||
|
: [absoluteUrl("/images/SeTrip.png")],
|
||||||
|
organizer: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: trip.organizer.name,
|
||||||
|
},
|
||||||
|
offers: {
|
||||||
|
"@type": "Offer",
|
||||||
|
url: tripUrl,
|
||||||
|
price: trip.price,
|
||||||
|
priceCurrency: "IDR",
|
||||||
|
availability: offerAvailability,
|
||||||
|
validFrom: trip.createdAt.toISOString(),
|
||||||
|
},
|
||||||
|
maximumAttendeeCapacity: trip.maxParticipants,
|
||||||
|
...(averageRating && trip.reviews.length > 0
|
||||||
|
? {
|
||||||
|
aggregateRating: {
|
||||||
|
"@type": "AggregateRating",
|
||||||
|
ratingValue: averageRating,
|
||||||
|
reviewCount: trip.reviews.length,
|
||||||
|
bestRating: 5,
|
||||||
|
worstRating: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
isAccessibleForFree: false,
|
||||||
|
inLanguage: "id-ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"@id": `${tripUrl}#breadcrumbs`,
|
||||||
|
itemListElement: [
|
||||||
|
{ "@type": "ListItem", position: 1, name: "Beranda", item: siteUrl },
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 2,
|
||||||
|
name: "Open Trip",
|
||||||
|
item: absoluteUrl("/trips"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 3,
|
||||||
|
name: trip.mountain,
|
||||||
|
item: tripUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
|
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="mb-3 flex items-center gap-2 text-xs text-neutral-500 sm:mb-4 sm:text-sm">
|
<div className="mb-3 flex items-center gap-2 text-xs text-neutral-500 sm:mb-4 sm:text-sm">
|
||||||
<Link href="/trips" className="hover:text-primary-600">
|
<Link href="/trips" className="hover:text-primary-600">
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { TripFilter } from "@/features/trip/components/trip-filter";
|
import { TripFilter } from "@/features/trip/components/trip-filter";
|
||||||
|
import { siteConfig } from "@/lib/site";
|
||||||
|
|
||||||
interface TripsPageProps {
|
interface TripsPageProps {
|
||||||
searchParams: Promise<{ q?: string; from?: string; to?: string }>;
|
searchParams: Promise<{ q?: string; from?: string; to?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
searchParams,
|
||||||
|
}: TripsPageProps): Promise<Metadata> {
|
||||||
|
const { q } = await searchParams;
|
||||||
|
const title = q
|
||||||
|
? `Open Trip "${q}" — Pendakian Bareng`
|
||||||
|
: "Open Trip Pendakian Gunung — Daftar Trip Aktif";
|
||||||
|
const description = q
|
||||||
|
? `Hasil pencarian open trip "${q}" di ${siteConfig.name}. Cari open trip pendakian, lihat tanggal, harga, & langsung gabung.`
|
||||||
|
: `Daftar open trip pendakian gunung yang sedang dibuka di ${siteConfig.name}. Pilih trip, lihat itinerary, dan langsung gabung mendaki bareng.`;
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: "/trips" },
|
||||||
|
openGraph: { title, description, url: "/trips" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const hasFilters = params.q || params.from || params.to;
|
const hasFilters = params.q || params.from || params.to;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export const siteConfig = {
|
||||||
|
name: "SeTrip",
|
||||||
|
brand: "SeTrip",
|
||||||
|
slogan: "Masa cowok sejati, cewek seimut, nggak SeTrip bareng?",
|
||||||
|
description:
|
||||||
|
"SeTrip adalah platform open trip pendakian gunung yang menyatukan open trip dari berbagai sosial media ke satu tempat. Cari, gabung, dan mendaki bareng — dari pemula sampai pendaki rutin.",
|
||||||
|
keywords: [
|
||||||
|
"setrip",
|
||||||
|
"se trip",
|
||||||
|
"open trip",
|
||||||
|
"open trip gunung",
|
||||||
|
"open trip pendakian",
|
||||||
|
"open trip jawa barat",
|
||||||
|
"hiking",
|
||||||
|
"hiking bareng",
|
||||||
|
"mendaki bersama",
|
||||||
|
"naik gunung bareng",
|
||||||
|
"pendakian gunung",
|
||||||
|
"trip gunung",
|
||||||
|
"trip pendakian",
|
||||||
|
"gabung open trip",
|
||||||
|
"cari open trip",
|
||||||
|
"papandayan",
|
||||||
|
"ciremai",
|
||||||
|
"gunung gede",
|
||||||
|
"gunung pangrango",
|
||||||
|
"gunung guntur",
|
||||||
|
"gunung malabar",
|
||||||
|
"gunung tangkuban parahu",
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const siteUrl = (
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"
|
||||||
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
|
export function absoluteUrl(path = "/"): string {
|
||||||
|
return `${siteUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
||||||
|
}
|
||||||
@@ -107,6 +107,31 @@ async function main() {
|
|||||||
const utc = (y: number, m0: number, d: number, h = 12, min = 0) =>
|
const utc = (y: number, m0: number, d: number, h = 12, min = 0) =>
|
||||||
new Date(Date.UTC(y, m0, d, h, min, 0, 0));
|
new Date(Date.UTC(y, m0, d, h, min, 0, 0));
|
||||||
|
|
||||||
|
// Unsplash mountain photos (URL CDN publik, gratis, stabil).
|
||||||
|
// Slug ID di komentar = id di unsplash.com/photos/{slug} buat ditelusuri ulang.
|
||||||
|
const img = (id: string) =>
|
||||||
|
`https://images.unsplash.com/photo-${id}?w=1200&q=80&auto=format&fit=crop`;
|
||||||
|
const MOUNTAIN_PHOTOS = {
|
||||||
|
papandayan1: img("1554629947-334ff61d85dc"), // xfngap_DToE
|
||||||
|
papandayan2: img("1464822759023-fed622ff2c3b"), // Bkci_8qcdvQ
|
||||||
|
papandayan3: img("1454496522488-7a8e488e8606"), // 9wg5jCEPBsw
|
||||||
|
ciremai1: img("1480497490787-505ec076689f"), // 6bKxagnIDtk
|
||||||
|
ciremai2: img("1483728642387-6c3bdd6c93e5"), // YFFGkE3y4F8
|
||||||
|
ciremai3: img("1502085671122-2d218cd434e6"), // NNmiv6zcFvk
|
||||||
|
gede1: img("1478059299873-f047d8c5fe1a"), // DXQB5D1njMY
|
||||||
|
gede2: img("1519681393784-d120267933ba"), // z8ct_Q3oCqM
|
||||||
|
gede3: img("1501785888041-af3ef285b470"), // T7K4aEPoGGk
|
||||||
|
gede4: img("1540979388789-6cee28a1cdc9"), // eUFfY6cwjSU
|
||||||
|
tangkuban1: img("1506905925346-21bda4d32df4"), // 1527pjeb6jg
|
||||||
|
tangkuban2: img("1490682143684-14369e18dce8"), // 8c6eS43iq1o
|
||||||
|
malabar1: img("1494548162494-384bba4ab999"), // xP_AGmeEa6s
|
||||||
|
malabar2: img("1500964757637-c85e8a162699"), // twukN12EN7c
|
||||||
|
malabar3: img("1549880181-56a44cf4a9a5"), // ePpaQC2c1xA
|
||||||
|
guntur1: img("1558883493-8b86ff880fec"), // vaG8rOJLDHo
|
||||||
|
guntur2: img("1554629947-334ff61d85dc"), // reuse — xfngap_DToE
|
||||||
|
guntur3: img("1464822759023-fed622ff2c3b"), // reuse — Bkci_8qcdvQ
|
||||||
|
} as const;
|
||||||
|
|
||||||
// --- Trip 1: Papandayan (by Dede Inoen) — 2 hari ---
|
// --- Trip 1: Papandayan (by Dede Inoen) — 2 hari ---
|
||||||
const trip1 = await prisma.trip.create({
|
const trip1 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -144,9 +169,9 @@ Minggu
|
|||||||
organizerId: dede.id,
|
organizerId: dede.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/papandayan-1.svg", caption: "Kawah Papandayan", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.papandayan1, caption: "Kawah Papandayan", order: 0 },
|
||||||
{ url: "/images/seed/papandayan-2.svg", caption: "Track menuju puncak", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.papandayan2, caption: "Track menuju puncak", order: 1 },
|
||||||
{ url: "/images/seed/papandayan-3.svg", caption: "Camping ground Pondok Salada", order: 2 },
|
{ url: MOUNTAIN_PHOTOS.papandayan3, caption: "Camping ground Pondok Salada", order: 2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -175,9 +200,9 @@ Itinerary:
|
|||||||
organizerId: panji.id,
|
organizerId: panji.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/ciremai-1.svg", caption: "Puncak Ciremai 3.078 mdpl", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.ciremai1, caption: "Puncak Ciremai 3.078 mdpl", order: 0 },
|
||||||
{ url: "/images/seed/ciremai-2.svg", caption: "Jalur pendakian via Apuy", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.ciremai2, caption: "Jalur pendakian via Apuy", order: 1 },
|
||||||
{ url: "/images/seed/ciremai-3.svg", caption: "Sunrise dari puncak", order: 2 },
|
{ url: MOUNTAIN_PHOTOS.ciremai3, caption: "Sunrise dari puncak", order: 2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -204,10 +229,10 @@ Start malam, summit saat sunrise. View epic dijamin!`,
|
|||||||
organizerId: fiersa.id,
|
organizerId: fiersa.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/gede-1.svg", caption: "Puncak Gunung Gede", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.gede1, caption: "Puncak Gunung Gede", order: 0 },
|
||||||
{ url: "/images/seed/gede-2.svg", caption: "Surya Kencana padang edelweis", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.gede2, caption: "Surya Kencana padang edelweis", order: 1 },
|
||||||
{ url: "/images/seed/gede-3.svg", caption: "Blue lake / Danau Biru", order: 2 },
|
{ url: MOUNTAIN_PHOTOS.gede3, caption: "Blue lake / Danau Biru", order: 2 },
|
||||||
{ url: "/images/seed/gede-4.svg", caption: "Night hike track Cibodas", order: 3 },
|
{ url: MOUNTAIN_PHOTOS.gede4, caption: "Night hike track Cibodas", order: 3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -234,8 +259,8 @@ Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
|
|||||||
organizerId: dede.id,
|
organizerId: dede.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/tangkuban-1.svg", caption: "Kawah Ratu", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.tangkuban1, caption: "Kawah Ratu", order: 0 },
|
||||||
{ url: "/images/seed/tangkuban-2.svg", caption: "Kawah Domas", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.tangkuban2, caption: "Kawah Domas", order: 1 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -262,9 +287,9 @@ Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
|
|||||||
organizerId: fiersa.id,
|
organizerId: fiersa.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/malabar-1.svg", caption: "Puncak Malabar malam hari", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.malabar1, caption: "Puncak Malabar malam hari", order: 0 },
|
||||||
{ url: "/images/seed/malabar-2.svg", caption: "View Bandung dari atas", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.malabar2, caption: "View Bandung dari atas", order: 1 },
|
||||||
{ url: "/images/seed/malabar-3.svg", caption: "Track pendakian", order: 2 },
|
{ url: MOUNTAIN_PHOTOS.malabar3, caption: "Track pendakian", order: 2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -291,9 +316,9 @@ Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`,
|
|||||||
organizerId: panji.id,
|
organizerId: panji.id,
|
||||||
images: {
|
images: {
|
||||||
create: [
|
create: [
|
||||||
{ url: "/images/seed/guntur-1.svg", caption: "Kawah aktif Gunung Guntur", order: 0 },
|
{ url: MOUNTAIN_PHOTOS.guntur1, caption: "Kawah aktif Gunung Guntur", order: 0 },
|
||||||
{ url: "/images/seed/guntur-2.svg", caption: "Jalur berbatu menuju puncak", order: 1 },
|
{ url: MOUNTAIN_PHOTOS.guntur2, caption: "Jalur berbatu menuju puncak", order: 1 },
|
||||||
{ url: "/images/seed/guntur-3.svg", caption: "View dari puncak", order: 2 },
|
{ url: MOUNTAIN_PHOTOS.guntur3, caption: "View dari puncak", order: 2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad4" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#67e8f9;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad4)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#0891b2" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#0e7490" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Ciremai</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 839 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad5" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#7dd3fc;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad5)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#0284c7" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#0369a1" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Ciremai</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 841 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad6" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#a5f3fc;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#06e3f0;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad6)"/>
|
|
||||||
<polygon points="0,410 210,210 410,340 610,170 800,385 800,500 0,500" fill="#00d9ff" opacity="0.7"/>
|
|
||||||
<polygon points="120,430 340,270 540,410 800,450 800,500 0,500" fill="#06b6d4" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Ciremai</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 841 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad7" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#93c5fd;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad7)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#1e3a8a" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#0f172a" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Gede</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 836 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad8" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#bfdbfe;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad8)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#1e40af" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#1f2937" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Gede</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 838 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad9" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#dbeafe;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#0c4a6e;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad9)"/>
|
|
||||||
<polygon points="0,410 210,210 410,340 610,170 800,385 800,500 0,500" fill="#082f49" opacity="0.7"/>
|
|
||||||
<polygon points="120,430 340,270 540,410 800,450 800,500 0,500" fill="#051e3e" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Gede</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 838 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad10" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#cffafe;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#164e63;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad10)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#0f2942" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#051924" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Gede</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 4</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 838 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad16" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#fed7aa;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#ea580c;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad16)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#c2410c" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#7c2d12" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Guntur</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 840 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad17" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#ffedd5;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#f97316;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad17)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#d97706" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#92400e" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Guntur</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 842 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad18" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#fecaca;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad18)"/>
|
|
||||||
<polygon points="0,410 210,210 410,340 610,170 800,385 800,500 0,500" fill="#dc2626" opacity="0.7"/>
|
|
||||||
<polygon points="120,430 340,270 540,410 800,450 800,500 0,500" fill="#991b1b" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Guntur</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 842 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad13" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#d8b4fe;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad13)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#6d28d9" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#3f0f5c" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Malabar</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 841 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad14" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#e9d5ff;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#a855f7;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad14)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#7e22ce" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#44005c" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Malabar</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 843 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad15" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#f3e8ff;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#c084fc;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad15)"/>
|
|
||||||
<polygon points="0,410 210,210 410,340 610,170 800,385 800,500 0,500" fill="#9333ea" opacity="0.7"/>
|
|
||||||
<polygon points="120,430 340,270 540,410 800,450 800,500 0,500" fill="#5b0f8f" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Malabar</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 843 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#4ade80;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#22c55e;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad1)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#16a34a" opacity="0.7"/>
|
|
||||||
<polygon points="150,420 350,250 550,400 800,450 800,500 0,500" fill="#15803d" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Papandayan</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 844 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#86efac;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#4ade80;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad2)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#22c55e" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#16a34a" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Papandayan</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 844 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad3" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#bbf7d0;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#6ee7b7;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad3)"/>
|
|
||||||
<polygon points="0,410 210,210 410,340 610,170 800,385 800,500 0,500" fill="#2dd4bf" opacity="0.7"/>
|
|
||||||
<polygon points="120,430 340,270 540,410 800,450 800,500 0,500" fill="#14b8a6" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Papandayan</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 844 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad11" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#bfef45;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#84cc16;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad11)"/>
|
|
||||||
<polygon points="0,400 200,200 400,350 600,180 800,380 800,500 0,500" fill="#65a30d" opacity="0.7"/>
|
|
||||||
<polygon points="0,450 250,270 500,400 800,460 800,500 0,500" fill="#3f6212" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Tangkuban</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 1</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 843 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad12" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#d4fc79;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#a3e635;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="800" height="500" fill="url(#grad12)"/>
|
|
||||||
<polygon points="0,420 180,240 380,360 580,200 800,390 800,500 0,500" fill="#79c21f" opacity="0.7"/>
|
|
||||||
<polygon points="100,440 320,280 520,420 800,460 800,500 0,500" fill="#4b5320" opacity="0.5"/>
|
|
||||||
<text x="400" y="100" font-size="48" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Tangkuban</text>
|
|
||||||
<text x="400" y="150" font-size="24" text-anchor="middle" fill="white" font-family="Arial, sans-serif">Mountain Vista 2</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 845 B |