Compare commits

..

2 Commits

Author SHA1 Message Date
arifal b31fe675ae chore(release): 0.2.0 2026-04-27 02:14:14 +07:00
arifal d91e16b6ef 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>
2026-04-27 02:14:08 +07:00
33 changed files with 660 additions and 258 deletions
+13
View File
@@ -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;
}
+45 -3
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { SessionProvider } from "@/components/providers/session-provider";
import { Navbar } from "@/components/shared/navbar";
import { siteConfig, siteUrl } from "@/lib/site";
import "./globals.css";
const geistSans = Geist({
@@ -15,9 +16,50 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "SeTrip",
description:
"Cari open trip pendakian gunung, gabung bareng, nikmati petualangan ke gunung-gunung Jawa Barat.",
metadataBase: new URL(siteUrl),
title: {
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: {
icon: "/SeTrip.ico",
apple: "/images/SeTrip.png",
+13
View File
@@ -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;
}
+46
View File
@@ -1,7 +1,20 @@
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 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() {
const trips = await tripService.getOpenTrips();
@@ -25,8 +38,41 @@ export default async function HomePage() {
.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 */}
+6
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import Image from "next/image";
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 { 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() {
const session = await getServerSession(authOptions);
if (!session?.user) {
+12
View File
@@ -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;
}
+16
View File
@@ -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,
};
}
+43
View File
@@ -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];
}
+227
View File
@@ -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 }
);
}
+134
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import Link from "next/link";
@@ -6,6 +7,7 @@ import { tripService } from "@/server/services/trip.service";
import { trustService } from "@/server/services/trust.service";
import { formatRupiah } from "@/lib/utils";
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
@@ -18,6 +20,47 @@ import {
isTripDepartureDayPast,
} 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({
params,
}: {
@@ -84,8 +127,99 @@ export default async function TripDetailPage({
(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 (
<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 */}
<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">
+20
View File
@@ -1,13 +1,33 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import { tripService } from "@/server/services/trip.service";
import { TripCard } from "@/features/trip/components/trip-card";
import { TripFilter } from "@/features/trip/components/trip-filter";
import { siteConfig } from "@/lib/site";
interface TripsPageProps {
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) {
const params = await searchParams;
const hasFilters = params.q || params.from || params.to;
+39
View File
@@ -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}`}`;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "setrip",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "setrip",
"version": "0.1.0",
"version": "0.2.0",
"dependencies": {
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "setrip",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev",
+43 -18
View File
@@ -107,6 +107,31 @@ async function main() {
const utc = (y: number, m0: number, d: number, h = 12, min = 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 ---
const trip1 = await prisma.trip.create({
data: {
@@ -144,9 +169,9 @@ Minggu
organizerId: dede.id,
images: {
create: [
{ url: "/images/seed/papandayan-1.svg", caption: "Kawah Papandayan", order: 0 },
{ url: "/images/seed/papandayan-2.svg", caption: "Track menuju puncak", order: 1 },
{ url: "/images/seed/papandayan-3.svg", caption: "Camping ground Pondok Salada", order: 2 },
{ url: MOUNTAIN_PHOTOS.papandayan1, caption: "Kawah Papandayan", order: 0 },
{ url: MOUNTAIN_PHOTOS.papandayan2, caption: "Track menuju puncak", order: 1 },
{ url: MOUNTAIN_PHOTOS.papandayan3, caption: "Camping ground Pondok Salada", order: 2 },
],
},
},
@@ -175,9 +200,9 @@ Itinerary:
organizerId: panji.id,
images: {
create: [
{ url: "/images/seed/ciremai-1.svg", caption: "Puncak Ciremai 3.078 mdpl", order: 0 },
{ url: "/images/seed/ciremai-2.svg", caption: "Jalur pendakian via Apuy", order: 1 },
{ url: "/images/seed/ciremai-3.svg", caption: "Sunrise dari puncak", order: 2 },
{ url: MOUNTAIN_PHOTOS.ciremai1, caption: "Puncak Ciremai 3.078 mdpl", order: 0 },
{ url: MOUNTAIN_PHOTOS.ciremai2, caption: "Jalur pendakian via Apuy", order: 1 },
{ 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,
images: {
create: [
{ url: "/images/seed/gede-1.svg", caption: "Puncak Gunung Gede", order: 0 },
{ url: "/images/seed/gede-2.svg", caption: "Surya Kencana padang edelweis", order: 1 },
{ url: "/images/seed/gede-3.svg", caption: "Blue lake / Danau Biru", order: 2 },
{ url: "/images/seed/gede-4.svg", caption: "Night hike track Cibodas", order: 3 },
{ url: MOUNTAIN_PHOTOS.gede1, caption: "Puncak Gunung Gede", order: 0 },
{ url: MOUNTAIN_PHOTOS.gede2, caption: "Surya Kencana padang edelweis", order: 1 },
{ url: MOUNTAIN_PHOTOS.gede3, caption: "Blue lake / Danau Biru", order: 2 },
{ 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,
images: {
create: [
{ url: "/images/seed/tangkuban-1.svg", caption: "Kawah Ratu", order: 0 },
{ url: "/images/seed/tangkuban-2.svg", caption: "Kawah Domas", order: 1 },
{ url: MOUNTAIN_PHOTOS.tangkuban1, caption: "Kawah Ratu", order: 0 },
{ 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,
images: {
create: [
{ url: "/images/seed/malabar-1.svg", caption: "Puncak Malabar malam hari", order: 0 },
{ url: "/images/seed/malabar-2.svg", caption: "View Bandung dari atas", order: 1 },
{ url: "/images/seed/malabar-3.svg", caption: "Track pendakian", order: 2 },
{ url: MOUNTAIN_PHOTOS.malabar1, caption: "Puncak Malabar malam hari", order: 0 },
{ url: MOUNTAIN_PHOTOS.malabar2, caption: "View Bandung dari atas", order: 1 },
{ 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,
images: {
create: [
{ url: "/images/seed/guntur-1.svg", caption: "Kawah aktif Gunung Guntur", order: 0 },
{ url: "/images/seed/guntur-2.svg", caption: "Jalur berbatu menuju puncak", order: 1 },
{ url: "/images/seed/guntur-3.svg", caption: "View dari puncak", order: 2 },
{ url: MOUNTAIN_PHOTOS.guntur1, caption: "Kawah aktif Gunung Guntur", order: 0 },
{ url: MOUNTAIN_PHOTOS.guntur2, caption: "Jalur berbatu menuju puncak", order: 1 },
{ url: MOUNTAIN_PHOTOS.guntur3, caption: "View dari puncak", order: 2 },
],
},
},
-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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

-13
View File
@@ -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