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}
+
+
+ )}
+ {/* 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 ? (
+
+ ) : (
+
+ 🏔️
+
+ )}
{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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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);