From d0480df31ae86bf4c4b4200f8bf7bbb84a476f2c Mon Sep 17 00:00:00 2001 From: arifal Date: Thu, 16 Apr 2026 16:26:29 +0700 Subject: [PATCH] add trip image --- app/create-trip/page.tsx | 3 + app/generated/prisma/browser.ts | 5 + app/generated/prisma/client.ts | 5 + app/generated/prisma/internal/class.ts | 18 +- .../prisma/internal/prismaNamespace.ts | 90 +- .../prisma/internal/prismaNamespaceBrowser.ts | 13 +- app/generated/prisma/models.ts | 1 + app/generated/prisma/models/Trip.ts | 187 ++- app/generated/prisma/models/TripImage.ts | 1389 +++++++++++++++++ app/globals.css | 4 - app/layout.tsx | 1 + app/page.tsx | 6 +- app/trips/[id]/page.tsx | 49 +- app/trips/page.tsx | 1 + features/trip/actions.ts | 8 +- features/trip/components/image-gallery.tsx | 76 + features/trip/components/image-url-input.tsx | 73 + features/trip/components/trip-card.tsx | 95 +- features/trip/schemas.ts | 1 - next.config.ts | 10 +- .../migration.sql | 22 + prisma/schema.prisma | 12 +- prisma/seed.ts | 230 +-- public/images/seed/ciremai-1.svg | 13 + public/images/seed/ciremai-2.svg | 13 + public/images/seed/ciremai-3.svg | 13 + public/images/seed/gede-1.svg | 13 + public/images/seed/gede-2.svg | 13 + public/images/seed/gede-3.svg | 13 + public/images/seed/gede-4.svg | 13 + public/images/seed/guntur-1.svg | 13 + public/images/seed/guntur-2.svg | 13 + public/images/seed/guntur-3.svg | 13 + public/images/seed/malabar-1.svg | 13 + public/images/seed/malabar-2.svg | 13 + public/images/seed/malabar-3.svg | 13 + public/images/seed/papandayan-1.svg | 13 + public/images/seed/papandayan-2.svg | 13 + public/images/seed/papandayan-3.svg | 13 + public/images/seed/tangkuban-1.svg | 13 + public/images/seed/tangkuban-2.svg | 13 + server/repositories/trip.repo.ts | 3 + server/services/trip.service.ts | 12 +- 43 files changed, 2334 insertions(+), 214 deletions(-) create mode 100644 app/generated/prisma/models/TripImage.ts create mode 100644 features/trip/components/image-gallery.tsx create mode 100644 features/trip/components/image-url-input.tsx create mode 100644 prisma/migrations/20260416080144_add_trip_images/migration.sql create mode 100644 public/images/seed/ciremai-1.svg create mode 100644 public/images/seed/ciremai-2.svg create mode 100644 public/images/seed/ciremai-3.svg create mode 100644 public/images/seed/gede-1.svg create mode 100644 public/images/seed/gede-2.svg create mode 100644 public/images/seed/gede-3.svg create mode 100644 public/images/seed/gede-4.svg create mode 100644 public/images/seed/guntur-1.svg create mode 100644 public/images/seed/guntur-2.svg create mode 100644 public/images/seed/guntur-3.svg create mode 100644 public/images/seed/malabar-1.svg create mode 100644 public/images/seed/malabar-2.svg create mode 100644 public/images/seed/malabar-3.svg create mode 100644 public/images/seed/papandayan-1.svg create mode 100644 public/images/seed/papandayan-2.svg create mode 100644 public/images/seed/papandayan-3.svg create mode 100644 public/images/seed/tangkuban-1.svg create mode 100644 public/images/seed/tangkuban-2.svg diff --git a/app/create-trip/page.tsx b/app/create-trip/page.tsx index a02974b..5e6c454 100644 --- a/app/create-trip/page.tsx +++ b/app/create-trip/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import Link from "next/link"; import { createTripAction } from "@/features/trip/actions"; +import { ImageUrlInput } from "@/features/trip/components/image-url-input"; const SAMPLE_MOUNTAINS = [ { name: "Gunung Papandayan", location: "Garut, Jawa Barat" }, @@ -172,6 +173,8 @@ export default function CreateTripPage() { /> + +
- {upcomingTrips.slice(0, 3).map((trip) => ( + {upcomingTrips.slice(0, 3).map((trip, i) => ( ))}
@@ -161,6 +163,7 @@ export default async function HomePage() { participantCount={trip._count.participants} organizerName={trip.organizer.name} status={trip.status} + coverImage={trip.images[0]?.url} /> ))} @@ -197,6 +200,7 @@ export default async function HomePage() { participantCount={trip._count.participants} organizerName={trip.organizer.name} status={trip.status} + coverImage={trip.images[0]?.url} /> ))} diff --git a/app/trips/[id]/page.tsx b/app/trips/[id]/page.tsx index 8859689..6c0397f 100644 --- a/app/trips/[id]/page.tsx +++ b/app/trips/[id]/page.tsx @@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth"; import { tripService } from "@/server/services/trip.service"; import { formatRupiah, formatDate } from "@/lib/utils"; import { JoinTripButton } from "@/features/trip/components/join-trip-button"; +import { ImageGallery } from "@/features/trip/components/image-gallery"; export default async function TripDetailPage({ params, @@ -49,33 +50,35 @@ export default async function TripDetailPage({
- {/* Header — dark theme */} -
-
-
-
-
-

{trip.title}

-

- 🏔️ {trip.mountain} -

-
- - {trip.status} - + {/* Image Gallery */} + + + {/* Title bar */} +
+
+
+

+ {trip.title} +

+

+ 🏔️ {trip.mountain} +

+ + {trip.status} +
-
+
{/* Info Grid */}
diff --git a/app/trips/page.tsx b/app/trips/page.tsx index d405fbe..0b6f6b9 100644 --- a/app/trips/page.tsx +++ b/app/trips/page.tsx @@ -57,6 +57,7 @@ export default async function TripsPage() { participantCount={trip._count.participants} organizerName={trip.organizer.name} status={trip.status} + coverImage={trip.images[0]?.url} /> ))}
diff --git a/features/trip/actions.ts b/features/trip/actions.ts index c928c61..81e156d 100644 --- a/features/trip/actions.ts +++ b/features/trip/actions.ts @@ -20,7 +20,6 @@ export async function createTripAction(formData: FormData) { date: formData.get("date") as string, maxParticipants: formData.get("maxParticipants") as string, price: formData.get("price") as string, - image: formData.get("image") as string, }; const result = createTripSchema.safeParse(raw); @@ -28,11 +27,18 @@ export async function createTripAction(formData: FormData) { return { error: result.error.issues[0].message }; } + // Collect image URLs from form (multiple inputs named "imageUrls") + const imageUrls = formData + .getAll("imageUrls") + .map((v) => (v as string).trim()) + .filter(Boolean); + try { const trip = await tripService.createTrip({ ...result.data, date: new Date(result.data.date), organizerId: session.user.id, + imageUrls: imageUrls.length > 0 ? imageUrls : undefined, }); revalidatePath("/trips"); return { success: true, tripId: trip.id }; diff --git a/features/trip/components/image-gallery.tsx b/features/trip/components/image-gallery.tsx new file mode 100644 index 0000000..ff8ed26 --- /dev/null +++ b/features/trip/components/image-gallery.tsx @@ -0,0 +1,76 @@ +"use client"; + +import Image from "next/image"; +import { useState } from "react"; + +interface TripImage { + id: string; + url: string; + caption: string | null; +} + +export function ImageGallery({ images }: { images: TripImage[] }) { + const [activeIndex, setActiveIndex] = useState(0); + + if (images.length === 0) { + return ( +
+ 🏔️ +
+ ); + } + + const activeImage = images[activeIndex]; + + return ( +
+ {/* Main Image */} +
+ {activeImage.caption + {activeImage.caption && ( +
+

+ {activeImage.caption} +

+
+ )} + {/* Counter */} +
+ {activeIndex + 1} / {images.length} +
+
+ + {/* Thumbnails */} + {images.length > 1 && ( +
+ {images.map((img, i) => ( + + ))} +
+ )} +
+ ); +} diff --git a/features/trip/components/image-url-input.tsx b/features/trip/components/image-url-input.tsx new file mode 100644 index 0000000..892d863 --- /dev/null +++ b/features/trip/components/image-url-input.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; + +export function ImageUrlInput() { + const [urls, setUrls] = useState([""]); + + function addField() { + if (urls.length < 5) { + setUrls([...urls, ""]); + } + } + + function removeField(index: number) { + setUrls(urls.filter((_, i) => i !== index)); + } + + function updateField(index: number, value: string) { + const updated = [...urls]; + updated[index] = value; + setUrls(updated); + } + + return ( +
+ +
+ {urls.map((url, i) => ( +
+ updateField(i, e.target.value)} + className="flex-1 rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white" + placeholder={ + i === 0 + ? "URL foto utama (cover)" + : `URL foto ${i + 1} (opsional)` + } + /> + {urls.length > 1 && ( + + )} +
+ ))} +
+ {urls.length < 5 && ( + + )} +

+ Upload foto ke hosting (imgur, imgbb, dll) lalu paste URL-nya di sini +

+
+ ); +} diff --git a/features/trip/components/trip-card.tsx b/features/trip/components/trip-card.tsx index afc8994..084606f 100644 --- a/features/trip/components/trip-card.tsx +++ b/features/trip/components/trip-card.tsx @@ -1,3 +1,4 @@ +import Image from "next/image"; import Link from "next/link"; import { formatRupiah, formatDate } from "@/lib/utils"; @@ -12,6 +13,8 @@ interface TripCardProps { participantCount: number; organizerName: string; status: string; + coverImage?: string | null; + priority?: boolean; } export function TripCard({ @@ -25,58 +28,76 @@ export function TripCard({ participantCount, organizerName, status, + coverImage, + priority, }: TripCardProps) { const spotsLeft = maxParticipants - participantCount; return ( -
- {/* Header */} -
-
-

- {title} -

-

{mountain}

-
+
+ {/* Cover Image */} +
+ {coverImage ? ( + {title} + ) : ( +
+ 🏔️ +
+ )} {status}
- {/* Info */} -
-
- 📍 {location} -
-
- 📅 {formatDate(date)} -
-
- 👤 {organizerName} -
-
+ {/* Content */} +
+

+ {title} +

+

{mountain}

- {/* Footer */} -
- - {formatRupiah(price)} - - 0 ? "text-secondary-600" : "text-amber-600" - }`} - > - {spotsLeft > 0 ? `${spotsLeft} slot tersisa` : "Penuh"} - +
+
+ 📍 {location} +
+
+ 📅{" "} + {formatDate(date)} +
+
+ 👤{" "} + {organizerName} +
+
+ +
+ + {formatRupiah(price)} + + 0 ? "text-secondary-600" : "text-amber-600" + }`} + > + {spotsLeft > 0 ? `${spotsLeft} slot tersisa` : "Penuh"} + +
diff --git a/features/trip/schemas.ts b/features/trip/schemas.ts index bb20b65..f3a310c 100644 --- a/features/trip/schemas.ts +++ b/features/trip/schemas.ts @@ -8,7 +8,6 @@ export const createTripSchema = z.object({ date: z.string().refine((val) => !isNaN(Date.parse(val)), "Tanggal tidak valid"), maxParticipants: z.coerce.number().min(1, "Minimal 1 peserta"), price: z.coerce.number().min(0, "Harga tidak valid"), - image: z.string().optional(), }); export type CreateTripInput = z.infer; diff --git a/next.config.ts b/next.config.ts index e9ffa30..289ec70 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + dangerouslyAllowSVG: true, + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + }, }; export default nextConfig; diff --git a/prisma/migrations/20260416080144_add_trip_images/migration.sql b/prisma/migrations/20260416080144_add_trip_images/migration.sql new file mode 100644 index 0000000..5cebda8 --- /dev/null +++ b/prisma/migrations/20260416080144_add_trip_images/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to drop the column `image` on the `Trip` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Trip" DROP COLUMN "image"; + +-- CreateTable +CREATE TABLE "TripImage" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL, + "caption" TEXT, + "order" INTEGER NOT NULL DEFAULT 0, + "tripId" TEXT NOT NULL, + + CONSTRAINT "TripImage_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "TripImage" ADD CONSTRAINT "TripImage_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "Trip"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5cfff75..ed66e1e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,7 +29,6 @@ model Trip { date DateTime maxParticipants Int price Int - image String? status TripStatus @default(OPEN) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -38,6 +37,17 @@ model Trip { organizer User @relation(fields: [organizerId], references: [id]) participants TripParticipant[] + images TripImage[] +} + +model TripImage { + id String @id @default(cuid()) + url String + caption String? + order Int @default(0) + + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) } model TripParticipant { diff --git a/prisma/seed.ts b/prisma/seed.ts index 62c5ec3..df9cb37 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -11,81 +11,93 @@ const prisma = new PrismaClient({ adapter }); async function main() { console.log("🌱 Seeding database...\n"); + // Clean existing data (order matters for FK) + 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 organizer1 = await prisma.user.upsert({ - where: { email: "andi@setrip.id" }, - update: {}, - create: { - name: "Andi Pendaki", - email: "andi@setrip.id", + const dede = await prisma.user.create({ + data: { + name: "Dede Inoen", + email: "dede.inoen@setrip.id", password, }, }); - const organizer2 = await prisma.user.upsert({ - where: { email: "rina@setrip.id" }, - update: {}, - create: { - name: "Rina Explorer", - email: "rina@setrip.id", + const panji = await prisma.user.create({ + data: { + name: "Panji Petualang", + email: "panji@setrip.id", password, }, }); - // User biasa (join trip) - const user1 = await prisma.user.upsert({ - where: { email: "budi@gmail.com" }, - update: {}, - create: { + 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 user2 = await prisma.user.upsert({ - where: { email: "sari@gmail.com" }, - update: {}, - create: { + const sari = await prisma.user.create({ + data: { name: "Sari Dewi", email: "sari@gmail.com", password, }, }); - const user3 = await prisma.user.upsert({ - where: { email: "doni@gmail.com" }, - update: {}, - create: { + const doni = await prisma.user.create({ + data: { name: "Doni Prasetyo", email: "doni@gmail.com", password, }, }); - const user4 = await prisma.user.upsert({ - where: { email: "maya@gmail.com" }, - update: {}, - create: { + 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: andi@setrip.id, rina@setrip.id"); - console.log(" Users: budi@gmail.com, sari@gmail.com, doni@gmail.com, maya@gmail.com"); + 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 ==================== + // ==================== TRIPS + IMAGES ==================== const now = new Date(); + const day = 24 * 60 * 60 * 1000; + // --- Trip 1: Papandayan (by Dede Inoen) --- const trip1 = await prisma.trip.create({ data: { title: "Open Trip Papandayan Weekend", @@ -100,14 +112,22 @@ Itinerary: - Minggu: Sunrise → Turun → Pulang`, mountain: "Gunung Papandayan", location: "Garut, Jawa Barat", - date: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), // 3 hari lagi + date: new Date(now.getTime() + 3 * day), maxParticipants: 10, price: 250000, status: "OPEN", - organizerId: organizer1.id, + 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 }, + ], + }, }, }); + // --- Trip 2: Ciremai (by Panji Petualang) --- const trip2 = await prisma.trip.create({ data: { title: "Pendakian Ciremai via Apuy", @@ -122,14 +142,22 @@ Itinerary: - Hari 2: Summit attack → Turun → Pulang`, mountain: "Gunung Ciremai", location: "Kuningan, Jawa Barat", - date: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000), // 5 hari lagi + date: new Date(now.getTime() + 5 * day), maxParticipants: 8, price: 350000, status: "OPEN", - organizerId: organizer1.id, + 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 }, + ], + }, }, }); + // --- Trip 3: Gede-Pangrango (by Fiersa Besari) --- const trip3 = await prisma.trip.create({ data: { title: "Sunrise Trip Gede-Pangrango", @@ -142,14 +170,23 @@ Itinerary: Start malam, summit saat sunrise. View epic dijamin!`, mountain: "Gunung Gede", location: "Bogor/Cianjur, Jawa Barat", - date: new Date(now.getTime() + 6 * 24 * 60 * 60 * 1000), // 6 hari lagi + date: new Date(now.getTime() + 6 * day), maxParticipants: 12, price: 280000, status: "OPEN", - organizerId: organizer2.id, + 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 }, + ], + }, }, }); + // --- Trip 4: Tangkuban Parahu (by Dede Inoen) --- const trip4 = await prisma.trip.create({ data: { title: "Trip Hemat Tangkuban Parahu", @@ -162,14 +199,21 @@ Start malam, summit saat sunrise. View epic dijamin!`, Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`, mountain: "Gunung Tangkuban Parahu", location: "Bandung, Jawa Barat", - date: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000), // 2 hari lagi + date: new Date(now.getTime() + 2 * day), maxParticipants: 15, price: 120000, status: "OPEN", - organizerId: organizer2.id, + 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 }, + ], + }, }, }); + // --- Trip 5: Malabar (by Fiersa Besari) --- const trip5 = await prisma.trip.create({ data: { title: "Malabar Night Hike", @@ -182,14 +226,22 @@ Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`, Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`, mountain: "Gunung Malabar", location: "Bandung, Jawa Barat", - date: new Date(now.getTime() + 4 * 24 * 60 * 60 * 1000), // 4 hari lagi + date: new Date(now.getTime() + 4 * day), maxParticipants: 10, price: 150000, status: "OPEN", - organizerId: organizer1.id, + 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 }, + ], + }, }, }); + // --- Trip 6: Guntur (by Panji Petualang) --- const trip6 = await prisma.trip.create({ data: { title: "Guntur Challenge Trip", @@ -202,72 +254,62 @@ Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`, Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`, mountain: "Gunung Guntur", location: "Garut, Jawa Barat", - date: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000), // 10 hari lagi + date: new Date(now.getTime() + 10 * day), maxParticipants: 8, price: 300000, status: "OPEN", - organizerId: organizer2.id, + 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 }, + ], + }, }, }); - console.log("✅ 6 Trips created\n"); + console.log("✅ 6 Trips + images created\n"); // ==================== PARTICIPANTS ==================== - // Trip 1 (Papandayan) — 3 peserta await prisma.tripParticipant.createMany({ data: [ - { tripId: trip1.id, userId: user1.id, status: "CONFIRMED" }, - { tripId: trip1.id, userId: user2.id, status: "CONFIRMED" }, - { tripId: trip1.id, userId: user3.id, status: "CONFIRMED" }, + // 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 ], }); - // Trip 2 (Ciremai) — 2 peserta - await prisma.tripParticipant.createMany({ - data: [ - { tripId: trip2.id, userId: user1.id, status: "CONFIRMED" }, - { tripId: trip2.id, userId: user4.id, status: "CONFIRMED" }, - ], - }); - - // Trip 3 (Gede) — 4 peserta - await prisma.tripParticipant.createMany({ - data: [ - { tripId: trip3.id, userId: user1.id, status: "CONFIRMED" }, - { tripId: trip3.id, userId: user2.id, status: "CONFIRMED" }, - { tripId: trip3.id, userId: user3.id, status: "CONFIRMED" }, - { tripId: trip3.id, userId: user4.id, status: "CONFIRMED" }, - ], - }); - - // Trip 4 (Tangkuban Parahu) — 5 peserta - await prisma.tripParticipant.createMany({ - data: [ - { tripId: trip4.id, userId: user1.id, status: "CONFIRMED" }, - { tripId: trip4.id, userId: user2.id, status: "CONFIRMED" }, - { tripId: trip4.id, userId: user3.id, status: "CONFIRMED" }, - { tripId: trip4.id, userId: user4.id, status: "CONFIRMED" }, - { tripId: trip4.id, userId: organizer1.id, status: "CONFIRMED" }, - ], - }); - - // Trip 5 (Malabar) — 1 peserta - await prisma.tripParticipant.createMany({ - data: [ - { tripId: trip5.id, userId: user2.id, status: "CONFIRMED" }, - ], - }); - - // Trip 6 (Guntur) — belum ada peserta - - console.log("✅ Participants joined trips"); - console.log(" Papandayan: 3/10 peserta"); - console.log(" Ciremai: 2/8 peserta"); - console.log(" Gede: 4/12 peserta"); - console.log(" Tangkuban Parahu: 5/15 peserta"); - console.log(" Malabar: 1/10 peserta"); - console.log(" Guntur: 0/8 peserta\n"); + 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!"); } diff --git a/public/images/seed/ciremai-1.svg b/public/images/seed/ciremai-1.svg new file mode 100644 index 0000000..9f3a310 --- /dev/null +++ b/public/images/seed/ciremai-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Ciremai + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/ciremai-2.svg b/public/images/seed/ciremai-2.svg new file mode 100644 index 0000000..d4fcc14 --- /dev/null +++ b/public/images/seed/ciremai-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Ciremai + Mountain Vista 2 + \ No newline at end of file diff --git a/public/images/seed/ciremai-3.svg b/public/images/seed/ciremai-3.svg new file mode 100644 index 0000000..8da47b5 --- /dev/null +++ b/public/images/seed/ciremai-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Ciremai + Mountain Vista 3 + \ No newline at end of file diff --git a/public/images/seed/gede-1.svg b/public/images/seed/gede-1.svg new file mode 100644 index 0000000..263d4ae --- /dev/null +++ b/public/images/seed/gede-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Gede + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/gede-2.svg b/public/images/seed/gede-2.svg new file mode 100644 index 0000000..fdce14e --- /dev/null +++ b/public/images/seed/gede-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Gede + Mountain Vista 2 + \ No newline at end of file diff --git a/public/images/seed/gede-3.svg b/public/images/seed/gede-3.svg new file mode 100644 index 0000000..c758333 --- /dev/null +++ b/public/images/seed/gede-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Gede + Mountain Vista 3 + \ No newline at end of file diff --git a/public/images/seed/gede-4.svg b/public/images/seed/gede-4.svg new file mode 100644 index 0000000..5dfe1ad --- /dev/null +++ b/public/images/seed/gede-4.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Gede + Mountain Vista 4 + \ No newline at end of file diff --git a/public/images/seed/guntur-1.svg b/public/images/seed/guntur-1.svg new file mode 100644 index 0000000..9fe1b33 --- /dev/null +++ b/public/images/seed/guntur-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Guntur + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/guntur-2.svg b/public/images/seed/guntur-2.svg new file mode 100644 index 0000000..e570ae7 --- /dev/null +++ b/public/images/seed/guntur-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Guntur + Mountain Vista 2 + \ No newline at end of file diff --git a/public/images/seed/guntur-3.svg b/public/images/seed/guntur-3.svg new file mode 100644 index 0000000..7fcdcb1 --- /dev/null +++ b/public/images/seed/guntur-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Guntur + Mountain Vista 3 + \ No newline at end of file diff --git a/public/images/seed/malabar-1.svg b/public/images/seed/malabar-1.svg new file mode 100644 index 0000000..55cdf0d --- /dev/null +++ b/public/images/seed/malabar-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Malabar + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/malabar-2.svg b/public/images/seed/malabar-2.svg new file mode 100644 index 0000000..c2205c3 --- /dev/null +++ b/public/images/seed/malabar-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Malabar + Mountain Vista 2 + \ No newline at end of file diff --git a/public/images/seed/malabar-3.svg b/public/images/seed/malabar-3.svg new file mode 100644 index 0000000..75053a9 --- /dev/null +++ b/public/images/seed/malabar-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Malabar + Mountain Vista 3 + \ No newline at end of file diff --git a/public/images/seed/papandayan-1.svg b/public/images/seed/papandayan-1.svg new file mode 100644 index 0000000..d858580 --- /dev/null +++ b/public/images/seed/papandayan-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Papandayan + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/papandayan-2.svg b/public/images/seed/papandayan-2.svg new file mode 100644 index 0000000..23ccae9 --- /dev/null +++ b/public/images/seed/papandayan-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Papandayan + Mountain Vista 2 + \ No newline at end of file diff --git a/public/images/seed/papandayan-3.svg b/public/images/seed/papandayan-3.svg new file mode 100644 index 0000000..adf116e --- /dev/null +++ b/public/images/seed/papandayan-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Papandayan + Mountain Vista 3 + \ No newline at end of file diff --git a/public/images/seed/tangkuban-1.svg b/public/images/seed/tangkuban-1.svg new file mode 100644 index 0000000..9e1261b --- /dev/null +++ b/public/images/seed/tangkuban-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Tangkuban + Mountain Vista 1 + \ No newline at end of file diff --git a/public/images/seed/tangkuban-2.svg b/public/images/seed/tangkuban-2.svg new file mode 100644 index 0000000..5f1838c --- /dev/null +++ b/public/images/seed/tangkuban-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Tangkuban + Mountain Vista 2 + \ No newline at end of file diff --git a/server/repositories/trip.repo.ts b/server/repositories/trip.repo.ts index dd42169..d54ca4b 100644 --- a/server/repositories/trip.repo.ts +++ b/server/repositories/trip.repo.ts @@ -6,6 +6,7 @@ export const tripRepo = { return prisma.trip.findMany({ include: { organizer: { select: { id: true, name: true, image: true } }, + images: { orderBy: { order: "asc" }, take: 1 }, _count: { select: { participants: true } }, }, orderBy: { date: "asc" }, @@ -17,6 +18,7 @@ export const tripRepo = { where: { status: "OPEN", date: { gte: new Date() } }, include: { organizer: { select: { id: true, name: true, image: true } }, + images: { orderBy: { order: "asc" }, take: 1 }, _count: { select: { participants: true } }, }, orderBy: { date: "asc" }, @@ -28,6 +30,7 @@ export const tripRepo = { where: { id }, include: { organizer: { select: { id: true, name: true, email: true, image: true } }, + images: { orderBy: { order: "asc" } }, participants: { include: { user: { select: { id: true, name: true, image: true } } }, }, diff --git a/server/services/trip.service.ts b/server/services/trip.service.ts index 68539a5..1acff23 100644 --- a/server/services/trip.service.ts +++ b/server/services/trip.service.ts @@ -9,8 +9,8 @@ interface CreateTripInput { date: Date; maxParticipants: number; price: number; - image?: string; organizerId: string; + imageUrls?: string[]; } export const tripService = { @@ -31,6 +31,12 @@ export const tripService = { }, async createTrip(input: CreateTripInput) { + const images = input.imageUrls?.length + ? { + create: input.imageUrls.map((url, i) => ({ url, order: i })), + } + : undefined; + return tripRepo.create({ title: input.title, description: input.description, @@ -39,8 +45,8 @@ export const tripService = { date: input.date, maxParticipants: input.maxParticipants, price: input.price, - image: input.image, organizer: { connect: { id: input.organizerId } }, + images, }); }, @@ -71,7 +77,6 @@ export const tripService = { const participant = await participantRepo.create(tripId, userId); - // Auto update status if full after join const newCount = await participantRepo.countByTrip(tripId); if (newCount >= trip.maxParticipants) { await tripRepo.updateStatus(tripId, "FULL"); @@ -88,7 +93,6 @@ export const tripService = { const result = await participantRepo.cancel(tripId, userId); - // Re-open trip if was full const trip = await tripRepo.findById(tripId); if (trip && trip.status === "FULL") { const count = await participantRepo.countByTrip(tripId);