Files
setrip/prisma/seed.ts
T
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

383 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "dotenv/config";
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import bcrypt from "bcryptjs";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const prisma = new PrismaClient({ adapter });
async function main() {
console.log("🌱 Seeding database...\n");
// Clean existing data (order matters for FK)
await prisma.tripReview.deleteMany();
await prisma.tripParticipant.deleteMany();
await prisma.tripImage.deleteMany();
await prisma.trip.deleteMany();
await prisma.user.deleteMany();
// ==================== USERS ====================
const password = await bcrypt.hash("password123", 12);
// Organizer
const dede = await prisma.user.create({
data: {
name: "Dede Inoen",
email: "dede.inoen@setrip.id",
password,
isVerified: true,
},
});
const panji = await prisma.user.create({
data: {
name: "Panji Petualang",
email: "panji@setrip.id",
password,
isVerified: true,
},
});
const fiersa = await prisma.user.create({
data: {
name: "Fiersa Besari",
email: "fiersa@setrip.id",
password,
},
});
// User biasa (peserta)
const budi = await prisma.user.create({
data: {
name: "Budi Santoso",
email: "budi@gmail.com",
password,
},
});
const sari = await prisma.user.create({
data: {
name: "Sari Dewi",
email: "sari@gmail.com",
password,
},
});
const doni = await prisma.user.create({
data: {
name: "Doni Prasetyo",
email: "doni@gmail.com",
password,
},
});
const maya = await prisma.user.create({
data: {
name: "Maya Putri",
email: "maya@gmail.com",
password,
},
});
const raka = await prisma.user.create({
data: {
name: "Raka Aditya",
email: "raka@gmail.com",
password,
},
});
console.log("✅ Users created");
console.log(" Organizer: dede.inoen@setrip.id, panji@setrip.id, fiersa@setrip.id");
console.log(" Peserta: budi, sari, doni, maya, raka @gmail.com");
console.log(" Password semua: password123\n");
// ==================== TRIPS + IMAGES ====================
/**
* Tanggal disimpan eksplisit di UTC agar filter `from`/`to` (YYYY-MM-DD UTC)
* cocok dengan yang tampil di daftar.
*
* - Multi hari: isi `endDate` = hari terakhir trip (UTC).
* - Satu hari / night hike satu malam: `endDate` null — filter memakai instan `date`
* dalam rentang hari UTC yang sama (jam tetap masuk hari itu).
*/
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: {
title: "Open Trip Papandayan Weekend",
description: `Pendakian santai ke Gunung Papandayan, cocok untuk pemula!
⚠️ Bawa: Sleeping bag, jaket, headlamp, air 2L`,
meetingPoint:
"Alun-alun Garut (depan pendopo), Sabtu 05:00 WIB — detail grup WA.",
itinerary: `Sabtu
• 05:00 Meeting & briefing
• 07:00 Berangkat menuju basecamp
• 12:00 Makan siang trail
• 15:00 Camp area Pondok Salada
Minggu
• 04:00 Summit attack
• 08:00 Sarap & packing
• 11:00 Turun
• 16:00 Estimasi kembali ke Garut`,
whatsIncluded: `• Transport PP Garutbasecamp
• Guide lokal
• Tenda tim (kapasitas sesuai muatan)
• Konsumsi: makan 3x + snack`,
whatsExcluded: `• Tiket masuk TNGGP
• Sleeping bag & matras pribadi
• Asuransi perjalanan`,
mountain: "Gunung Papandayan",
location: "Garut, Jawa Barat",
date: utc(2026, 3, 23, 8, 0),
endDate: utc(2026, 3, 24, 18, 0),
maxParticipants: 10,
price: 250000,
status: "OPEN",
organizerId: dede.id,
images: {
create: [
{ 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 },
],
},
},
});
// --- Trip 2: Ciremai (by Panji Petualang) — 2 hari ---
const trip2 = await prisma.trip.create({
data: {
title: "Pendakian Ciremai via Apuy",
description: `Trip ke puncak tertinggi Jawa Barat! 🏔️
📍 Meeting Point: Stasiun Kuningan, 04:00 WIB
🎒 Fasilitas: Transport lokal, guide, logistik
⚠️ Level: Menengah — perlu stamina baik
Itinerary:
- Hari 1: Basecamp → Pos 3 → Camp
- Hari 2: Summit attack → Turun → Pulang`,
mountain: "Gunung Ciremai",
location: "Kuningan, Jawa Barat",
date: utc(2026, 3, 25, 4, 0),
endDate: utc(2026, 3, 26, 18, 0),
maxParticipants: 8,
price: 350000,
status: "OPEN",
organizerId: panji.id,
images: {
create: [
{ 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 },
],
},
},
});
// --- Trip 3: Gede-Pangrango (by Fiersa Besari) — 2 hari ---
const trip3 = await prisma.trip.create({
data: {
title: "Sunrise Trip Gede-Pangrango",
description: `Combo 2 puncak sekaligus! Gede + Pangrango.
📍 Meeting Point: Cibodas, 22:00 WIB (malam)
🎒 Fasilitas: Guide, tenda, makan
⚠️ Level: Advance — night hike
Start malam, summit saat sunrise. View epic dijamin!`,
mountain: "Gunung Gede",
location: "Bogor/Cianjur, Jawa Barat",
date: utc(2026, 3, 27, 22, 0),
endDate: utc(2026, 3, 28, 16, 0),
maxParticipants: 12,
price: 280000,
status: "OPEN",
organizerId: fiersa.id,
images: {
create: [
{ 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 },
],
},
},
});
// --- Trip 4: Tangkuban Parahu (by Dede Inoen) — 1 hari ---
const trip4 = await prisma.trip.create({
data: {
title: "Trip Hemat Tangkuban Parahu",
description: `Trip santai ke kawah Tangkuban Parahu. Cocok buat first-timer!
📍 Meeting Point: Lembang, 07:00 WIB
🎒 Fasilitas: Transport, snack, guide
⚠️ Level: Easy — bisa pakai sandal gunung
Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
mountain: "Gunung Tangkuban Parahu",
location: "Bandung, Jawa Barat",
date: utc(2026, 3, 22, 0, 0),
endDate: null,
maxParticipants: 15,
price: 120000,
status: "OPEN",
organizerId: dede.id,
images: {
create: [
{ url: MOUNTAIN_PHOTOS.tangkuban1, caption: "Kawah Ratu", order: 0 },
{ url: MOUNTAIN_PHOTOS.tangkuban2, caption: "Kawah Domas", order: 1 },
],
},
},
});
// --- Trip 5: Malabar (by Fiersa Besari) — 1 hari (night hike, `endDate` null) ---
const trip5 = await prisma.trip.create({
data: {
title: "Malabar Night Hike",
description: `Night hike ke Gunung Malabar — view kota Bandung dari atas!
📍 Meeting Point: Pangalengan, 20:00 WIB
🎒 Fasilitas: Guide, teh hangat di puncak
⚠️ Bawa: Headlamp WAJIB, jaket tebal
Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
mountain: "Gunung Malabar",
location: "Bandung, Jawa Barat",
date: utc(2026, 3, 20, 14, 0),
endDate: null,
maxParticipants: 10,
price: 150000,
status: "OPEN",
organizerId: fiersa.id,
images: {
create: [
{ 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 },
],
},
},
});
// --- Trip 6: Guntur (by Panji Petualang) — 2 hari ---
const trip6 = await prisma.trip.create({
data: {
title: "Guntur Challenge Trip",
description: `Trip ke Gunung Guntur — jalur menantang tapi worth it!
📍 Meeting Point: Alun-alun Garut, 04:30 WIB
🎒 Fasilitas: Transport, guide, logistik
⚠️ Level: Hard — medan berbatu & terjal
Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`,
mountain: "Gunung Guntur",
location: "Garut, Jawa Barat",
date: utc(2026, 3, 30, 4, 0),
endDate: utc(2026, 4, 1, 18, 0),
maxParticipants: 8,
price: 300000,
status: "OPEN",
organizerId: panji.id,
images: {
create: [
{ 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 },
],
},
},
});
console.log("✅ 6 Trips + images created (tanggal UTC + endDate untuk trip multi hari)\n");
console.log(
` Guntur: ${trip6.title} ${trip6.date.toISOString().slice(0, 10)}${trip6.endDate?.toISOString().slice(0, 10) ?? "-"}\n`
);
// ==================== PARTICIPANTS ====================
await prisma.tripParticipant.createMany({
data: [
// Papandayan — 4 peserta
{ tripId: trip1.id, userId: budi.id, status: "CONFIRMED" },
{ tripId: trip1.id, userId: sari.id, status: "CONFIRMED" },
{ tripId: trip1.id, userId: doni.id, status: "CONFIRMED" },
{ tripId: trip1.id, userId: raka.id, status: "CONFIRMED" },
// Ciremai — 2 peserta
{ tripId: trip2.id, userId: budi.id, status: "CONFIRMED" },
{ tripId: trip2.id, userId: maya.id, status: "CONFIRMED" },
// Gede — 5 peserta
{ tripId: trip3.id, userId: budi.id, status: "CONFIRMED" },
{ tripId: trip3.id, userId: sari.id, status: "CONFIRMED" },
{ tripId: trip3.id, userId: doni.id, status: "CONFIRMED" },
{ tripId: trip3.id, userId: maya.id, status: "CONFIRMED" },
{ tripId: trip3.id, userId: raka.id, status: "CONFIRMED" },
// Tangkuban Parahu — 5 peserta
{ tripId: trip4.id, userId: budi.id, status: "CONFIRMED" },
{ tripId: trip4.id, userId: sari.id, status: "CONFIRMED" },
{ tripId: trip4.id, userId: doni.id, status: "CONFIRMED" },
{ tripId: trip4.id, userId: maya.id, status: "CONFIRMED" },
{ tripId: trip4.id, userId: raka.id, status: "CONFIRMED" },
// Malabar — 2 peserta
{ tripId: trip5.id, userId: sari.id, status: "CONFIRMED" },
{ tripId: trip5.id, userId: maya.id, status: "CONFIRMED" },
// Guntur — 0 peserta
],
});
console.log("✅ Participants joined");
console.log(" Papandayan: 4/10 | Ciremai: 2/8 | Gede: 5/12");
console.log(" Tangkuban Parahu: 5/15 | Malabar: 2/10 | Guntur: 0/8\n");
console.log("🎉 Seed complete!");
}
main()
.catch((e) => {
console.error("❌ Seed failed:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});