auth, trips and join trips

This commit is contained in:
2026-04-16 14:51:54 +07:00
parent de0d1c5413
commit 237caad488
49 changed files with 11343 additions and 334 deletions
+28
View File
@@ -0,0 +1,28 @@
import { prisma } from "@/lib/prisma";
export const participantRepo = {
async findByTripAndUser(tripId: string, userId: string) {
return prisma.tripParticipant.findUnique({
where: { tripId_userId: { tripId, userId } },
});
},
async create(tripId: string, userId: string) {
return prisma.tripParticipant.create({
data: { tripId, userId, status: "CONFIRMED" },
});
},
async countByTrip(tripId: string) {
return prisma.tripParticipant.count({
where: { tripId, status: { not: "CANCELLED" } },
});
},
async cancel(tripId: string, userId: string) {
return prisma.tripParticipant.update({
where: { tripId_userId: { tripId, userId } },
data: { status: "CANCELLED" },
});
},
};
+45
View File
@@ -0,0 +1,45 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/app/generated/prisma/client";
export const tripRepo = {
async findAll() {
return prisma.trip.findMany({
include: {
organizer: { select: { id: true, name: true, image: true } },
_count: { select: { participants: true } },
},
orderBy: { date: "asc" },
});
},
async findOpen() {
return prisma.trip.findMany({
where: { status: "OPEN", date: { gte: new Date() } },
include: {
organizer: { select: { id: true, name: true, image: true } },
_count: { select: { participants: true } },
},
orderBy: { date: "asc" },
});
},
async findById(id: string) {
return prisma.trip.findUnique({
where: { id },
include: {
organizer: { select: { id: true, name: true, email: true, image: true } },
participants: {
include: { user: { select: { id: true, name: true, image: true } } },
},
},
});
},
async create(data: Prisma.TripCreateInput) {
return prisma.trip.create({ data });
},
async updateStatus(id: string, status: "OPEN" | "FULL" | "CLOSED" | "COMPLETED") {
return prisma.trip.update({ where: { id }, data: { status } });
},
};
+16
View File
@@ -0,0 +1,16 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/app/generated/prisma/client";
export const userRepo = {
async findByEmail(email: string) {
return prisma.user.findUnique({ where: { email } });
},
async findById(id: string) {
return prisma.user.findUnique({ where: { id } });
},
async create(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
},
};
+21
View File
@@ -0,0 +1,21 @@
import bcrypt from "bcryptjs";
import { userRepo } from "@/server/repositories/user.repo";
export const authService = {
async register(data: { name: string; email: string; password: string }) {
const existing = await userRepo.findByEmail(data.email);
if (existing) {
throw new Error("Email sudah terdaftar");
}
const hashedPassword = await bcrypt.hash(data.password, 12);
const user = await userRepo.create({
name: data.name,
email: data.email,
password: hashedPassword,
});
return { id: user.id, name: user.name, email: user.email };
},
};
+102
View File
@@ -0,0 +1,102 @@
import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
interface CreateTripInput {
title: string;
description?: string;
mountain: string;
location: string;
date: Date;
maxParticipants: number;
price: number;
image?: string;
organizerId: string;
}
export const tripService = {
async getOpenTrips() {
return tripRepo.findOpen();
},
async getAllTrips() {
return tripRepo.findAll();
},
async getTripById(id: string) {
const trip = await tripRepo.findById(id);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
return trip;
},
async createTrip(input: CreateTripInput) {
return tripRepo.create({
title: input.title,
description: input.description,
mountain: input.mountain,
location: input.location,
date: input.date,
maxParticipants: input.maxParticipants,
price: input.price,
image: input.image,
organizer: { connect: { id: input.organizerId } },
});
},
async joinTrip(tripId: string, userId: string) {
const trip = await tripRepo.findById(tripId);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (trip.status !== "OPEN") {
throw new Error("Trip tidak tersedia untuk pendaftaran");
}
if (trip.organizerId === userId) {
throw new Error("Organizer tidak bisa join trip sendiri");
}
const existing = await participantRepo.findByTripAndUser(tripId, userId);
if (existing && existing.status !== "CANCELLED") {
throw new Error("Kamu sudah terdaftar di trip ini");
}
const participantCount = await participantRepo.countByTrip(tripId);
if (participantCount >= trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "FULL");
throw new Error("Trip sudah penuh");
}
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");
}
return participant;
},
async cancelJoin(tripId: string, userId: string) {
const existing = await participantRepo.findByTripAndUser(tripId, userId);
if (!existing || existing.status === "CANCELLED") {
throw new Error("Kamu tidak terdaftar di trip ini");
}
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);
if (count < trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "OPEN");
}
}
return result;
},
};