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
+26
View File
@@ -1,5 +1,31 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
# 📄 Documentation Files
## AGENTS.md
Berisi:
- struktur project
- aturan coding
- best practices
## CLAUDE.md
Berisi:
- instruksi untuk AI assistant
- cara generate code
- constraint development
## Rules
- Semua developer wajib mengikuti AGENTS.md
- AI harus membaca AGENTS.md sebelum generate code
- Jangan duplikasi aturan di banyak file
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
+281
View File
@@ -0,0 +1,281 @@
# 🚀 SeTrip Project Architecture Guide
## 📌 Overview
Dokumen ini menjelaskan standar struktur project untuk aplikasi **SeTrip** menggunakan Next.js (App Router).
Tujuan:
- Menjaga code tetap rapi & scalable
- Menghindari spaghetti code
- Memudahkan AI / developer kontribusi
---
# 🧠 Core Principles
1. **Feature-based structure (WAJIB)**
2. **Separation client vs server logic**
3. **Colocation (file dekat dengan fitur)**
4. **No over-engineering (keep it simple)**
---
# 🏗️ Project Structure
```bash
src/
├── app/ # Next.js App Router (UI & routing)
│ ├── (public)/ # halaman publik
│ ├── (auth)/ # login/register
│ ├── trips/ # halaman trip
│ ├── create-trip/
│ └── profile/
├── features/ # 🔥 Business logic per domain
│ ├── auth/
│ ├── user/
│ ├── trip/
│ ├── booking/
│ └── review/
├── server/ # 🔒 Backend-only logic
│ ├── db/
│ ├── services/
│ └── repositories/
├── components/ # reusable UI components
│ ├── ui/
│ └── shared/
├── lib/ # helpers & utilities
│ ├── prisma.ts
│ ├── auth.ts
│ └── utils.ts
├── hooks/ # custom react hooks
├── types/ # global types
└── config/ # config (env, constants)
```
---
# 📂 Folder Rules
## 1. `/app` (Routing Layer)
Hanya untuk:
- page.tsx
- layout.tsx
- UI rendering
- data fetching ringan
❌ DILARANG:
- business logic
- query database langsung
---
## 2. `/features` (Domain Layer) 🔥
Setiap fitur punya folder sendiri:
```bash
features/trip/
├── components/
├── services/
├── hooks/
├── api/
└── types.ts
```
Digunakan untuk:
- business logic
- validation
- orchestration
---
## 3. `/server` (Backend Layer)
```bash
server/
├── db/
│ └── prisma.ts
├── services/
│ └── trip.service.ts
└── repositories/
└── trip.repo.ts
```
Digunakan untuk:
- database access
- heavy logic
- internal backend
---
## 4. `/components`
```bash
components/
├── ui/ # button, input, modal
└── shared/ # navbar, header
```
---
## 5. `/lib`
Utility global:
- prisma instance
- auth helper
- formatter
---
# 🔄 Data Flow Standard
```text
UI (app/)
Feature (features/)
Service (server/services)
Repository (server/repositories)
Database
```
---
# 🧩 Example Flow (Create Trip)
1. User klik create trip (UI)
2. Call feature service
3. Feature validate data
4. Call server service
5. Service call repository
6. Repository insert ke database
---
# ⚠️ Anti-Pattern (DILARANG)
❌ Jangan:
- query Prisma langsung di page.tsx
- campur UI + logic
- buat folder controller/service ala backend di Next.js
- over abstraction di awal
---
# ✅ Best Practice
✔ Gunakan:
- Zod untuk validation
- Prisma untuk ORM
- Server Actions / API route secukupnya
✔ Naming:
- `trip.service.ts`
- `trip.repo.ts`
- `useTrip.ts`
---
# 🚀 Development Phases
## Phase 1 (MVP)
- Auth
- Create Trip
- Join Trip
## Phase 2
- Review
- Limit & validation
## Phase 3
- Payment
- Organizer role
---
# 🧠 Final Principle
> Build fast → validate → refactor later
Jangan terlalu fokus ke arsitektur sempurna di awal.
---
# 📌 Notes for AI / Contributors
- Ikuti struktur ini tanpa pengecualian
- Jangan membuat folder baru tanpa alasan jelas
- Semua fitur baru harus masuk ke `/features`
- Semua akses DB harus lewat `/server`
---
# 📦 Tech Stack (MVP - Ideal)
Project SeTrip menggunakan stack berikut:
## Core
- Next.js (App Router)
- TypeScript
- Tailwind CSS
## Backend & Database
- Prisma (ORM)
- PostgreSQL
## Authentication
- NextAuth
## Validation
- Zod
## Utility
- Axios
- Dayjs
- Clsx
- Bcryptjs
---
# ⚠️ Rules for Dependencies
- Gunakan hanya library yang dibutuhkan untuk MVP
- Hindari menambahkan dependency tanpa alasan jelas
- Jangan install:
- state management kompleks (Redux, Zustand)
- realtime (socket.io)
- queue system
- payment gateway (di phase awal)
---
# 📌 Philosophy
> Minimal dependencies → faster development → easier maintenance
**End of Document**
+18 -225
View File
@@ -1,233 +1,26 @@
# 🚀 SeTrip Project Architecture Guide
# 🤖 Claude / AI Instructions
## 📌 Overview
## General Rules
Dokumen ini menjelaskan standar struktur project untuk aplikasi **SeTrip** menggunakan Next.js (App Router).
- Gunakan TypeScript
- Ikuti struktur feature-based
- Jangan letakkan business logic di `/app`
- Semua database access harus melalui `/server`
Tujuan:
## Code Rules
- Menjaga code tetap rapi & scalable
- Menghindari spaghetti code
- Memudahkan AI / developer kontribusi
- Gunakan Prisma untuk database
- Gunakan Zod untuk validation
- Gunakan service layer untuk business logic
---
## Forbidden
# 🧠 Core Principles
- Jangan query database langsung di component
- Jangan buat arsitektur over-engineered
- Jangan menambahkan dependency tanpa kebutuhan jelas
1. **Feature-based structure (WAJIB)**
2. **Separation client vs server logic**
3. **Colocation (file dekat dengan fitur)**
4. **No over-engineering (keep it simple)**
## Output Style
---
# 🏗️ Project Structure
```bash
src/
├── app/ # Next.js App Router (UI & routing)
│ ├── (public)/ # halaman publik
│ ├── (auth)/ # login/register
│ ├── trips/ # halaman trip
│ ├── create-trip/
│ └── profile/
├── features/ # 🔥 Business logic per domain
│ ├── auth/
│ ├── user/
│ ├── trip/
│ ├── booking/
│ └── review/
├── server/ # 🔒 Backend-only logic
│ ├── db/
│ ├── services/
│ └── repositories/
├── components/ # reusable UI components
│ ├── ui/
│ └── shared/
├── lib/ # helpers & utilities
│ ├── prisma.ts
│ ├── auth.ts
│ └── utils.ts
├── hooks/ # custom react hooks
├── types/ # global types
└── config/ # config (env, constants)
```
---
# 📂 Folder Rules
## 1. `/app` (Routing Layer)
Hanya untuk:
- page.tsx
- layout.tsx
- UI rendering
- data fetching ringan
❌ DILARANG:
- business logic
- query database langsung
---
## 2. `/features` (Domain Layer) 🔥
Setiap fitur punya folder sendiri:
```bash
features/trip/
├── components/
├── services/
├── hooks/
├── api/
└── types.ts
```
Digunakan untuk:
- business logic
- validation
- orchestration
---
## 3. `/server` (Backend Layer)
```bash
server/
├── db/
│ └── prisma.ts
├── services/
│ └── trip.service.ts
└── repositories/
└── trip.repo.ts
```
Digunakan untuk:
- database access
- heavy logic
- internal backend
---
## 4. `/components`
```bash
components/
├── ui/ # button, input, modal
└── shared/ # navbar, header
```
---
## 5. `/lib`
Utility global:
- prisma instance
- auth helper
- formatter
---
# 🔄 Data Flow Standard
```text
UI (app/)
Feature (features/)
Service (server/services)
Repository (server/repositories)
Database
```
---
# 🧩 Example Flow (Create Trip)
1. User klik create trip (UI)
2. Call feature service
3. Feature validate data
4. Call server service
5. Service call repository
6. Repository insert ke database
---
# ⚠️ Anti-Pattern (DILARANG)
❌ Jangan:
- query Prisma langsung di page.tsx
- campur UI + logic
- buat folder controller/service ala backend di Next.js
- over abstraction di awal
---
# ✅ Best Practice
✔ Gunakan:
- Zod untuk validation
- Prisma untuk ORM
- Server Actions / API route secukupnya
✔ Naming:
- `trip.service.ts`
- `trip.repo.ts`
- `useTrip.ts`
---
# 🚀 Development Phases
## Phase 1 (MVP)
- Auth
- Create Trip
- Join Trip
## Phase 2
- Review
- Limit & validation
## Phase 3
- Payment
- Organizer role
---
# 🧠 Final Principle
> Build fast → validate → refactor later
Jangan terlalu fokus ke arsitektur sempurna di awal.
---
# 📌 Notes for AI / Contributors
- Ikuti struktur ini tanpa pengecualian
- Jangan membuat folder baru tanpa alasan jelas
- Semua fitur baru harus masuk ke `/features`
- Semua akses DB harus lewat `/server`
---
**End of Document**
- Clean code
- Modular
- Readable
+6
View File
@@ -0,0 +1,6 @@
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
+229
View File
@@ -0,0 +1,229 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { createTripAction } from "@/features/trip/actions";
const SAMPLE_MOUNTAINS = [
{ name: "Gunung Papandayan", location: "Garut, Jawa Barat" },
{ name: "Gunung Ciremai", location: "Kuningan, Jawa Barat" },
{ name: "Gunung Pangrango", location: "Bogor/Cianjur, Jawa Barat" },
{ name: "Gunung Gede", location: "Bogor/Cianjur, Jawa Barat" },
{ name: "Gunung Tangkuban Parahu", location: "Bandung, Jawa Barat" },
{ name: "Gunung Bukit Tunggul", location: "Bandung, Jawa Barat" },
{ name: "Gunung Malabar", location: "Bandung, Jawa Barat" },
{ name: "Gunung Guntur", location: "Garut, Jawa Barat" },
];
export default function CreateTripPage() {
const { data: session } = useSession();
const router = useRouter();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
if (!session?.user) {
return (
<div className="flex min-h-[calc(100vh-3.5rem)] items-center justify-center px-4">
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-50 text-3xl">
🔒
</div>
<p className="mb-4 text-neutral-500">
Kamu harus login untuk membuat trip.
</p>
<Link
href="/login"
className="inline-block rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white hover:bg-primary-700"
>
Login
</Link>
</div>
</div>
);
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);
const formData = new FormData(e.currentTarget);
const result = await createTripAction(formData);
setLoading(false);
if (result.error) {
setError(result.error);
} else if (result.tripId) {
router.push(`/trips/${result.tripId}`);
}
}
function handleMountainSelect(e: React.ChangeEvent<HTMLSelectElement>) {
const selected = SAMPLE_MOUNTAINS.find((m) => m.name === e.target.value);
if (selected) {
const form = e.target.form;
if (form) {
const mountainInput = form.elements.namedItem(
"mountain"
) as HTMLInputElement;
const locationInput = form.elements.namedItem(
"location"
) as HTMLInputElement;
mountainInput.value = selected.name;
locationInput.value = selected.location;
}
}
}
return (
<div className="mx-auto max-w-2xl px-4 py-8">
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-800">Buat Trip Baru</h1>
<p className="mt-1 text-sm text-neutral-500">
Ajak teman baru naik gunung bareng!
</p>
</div>
<div className="rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Mountain Quick Picker */}
<div className="rounded-xl bg-primary-50 p-4">
<label className="mb-2 flex items-center gap-1.5 text-sm font-bold text-primary-800">
<span>🏔</span> Pilih Gunung Jawa Barat
</label>
<select
onChange={handleMountainSelect}
className="w-full rounded-lg border border-primary-200 bg-white px-4 py-2.5 text-sm text-neutral-800"
defaultValue=""
>
<option value="" disabled>
Pilih gunung...
</option>
{SAMPLE_MOUNTAINS.map((m) => (
<option key={m.name} value={m.name}>
{m.name} {m.location}
</option>
))}
</select>
</div>
<div>
<label htmlFor="title" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Judul Trip
</label>
<input
id="title"
name="title"
type="text"
required
className="w-full 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="contoh: Open Trip Papandayan Weekend"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="mountain" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nama Gunung
</label>
<input
id="mountain"
name="mountain"
type="text"
required
className="w-full 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="Gunung Papandayan"
/>
</div>
<div>
<label htmlFor="location" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Lokasi
</label>
<input
id="location"
name="location"
type="text"
required
className="w-full 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="Garut, Jawa Barat"
/>
</div>
</div>
<div>
<label htmlFor="description" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Deskripsi
</label>
<textarea
id="description"
name="description"
rows={4}
className="w-full 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="Detail trip, itinerary, meeting point, fasilitas..."
/>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div>
<label htmlFor="date" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Tanggal
</label>
<input
id="date"
name="date"
type="date"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 focus:bg-white"
/>
</div>
<div>
<label htmlFor="maxParticipants" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Maks Peserta
</label>
<input
id="maxParticipants"
name="maxParticipants"
type="number"
required
min={1}
className="w-full 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="10"
/>
</div>
<div>
<label htmlFor="price" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Harga (Rp)
</label>
<input
id="price"
name="price"
type="number"
required
min={0}
className="w-full 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="150000"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-xl bg-primary-600 py-3 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Membuat Trip..." : "Buat Trip"}
</button>
</form>
</div>
</div>
);
}
+34
View File
@@ -0,0 +1,34 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Trip
*
*/
export type Trip = Prisma.TripModel
/**
* Model TripParticipant
*
*/
export type TripParticipant = Prisma.TripParticipantModel
+58
View File
@@ -0,0 +1,58 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import * as $Class from "./internal/class"
import * as Prisma from "./internal/prismaNamespace"
export * as $Enums from './enums'
export * from "./enums"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Trip
*
*/
export type Trip = Prisma.TripModel
/**
* Model TripParticipant
*
*/
export type TripParticipant = Prisma.TripParticipantModel
+344
View File
@@ -0,0 +1,344 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import type * as Prisma from "./internal/prismaNamespace"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type EnumTripStatusFilter<$PrismaModel = never> = {
equals?: $Enums.TripStatus | Prisma.EnumTripStatusFieldRefInput<$PrismaModel>
in?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumTripStatusFilter<$PrismaModel> | $Enums.TripStatus
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type EnumTripStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.TripStatus | Prisma.EnumTripStatusFieldRefInput<$PrismaModel>
in?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumTripStatusWithAggregatesFilter<$PrismaModel> | $Enums.TripStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumTripStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumTripStatusFilter<$PrismaModel>
}
export type EnumParticipantStatusFilter<$PrismaModel = never> = {
equals?: $Enums.ParticipantStatus | Prisma.EnumParticipantStatusFieldRefInput<$PrismaModel>
in?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel> | $Enums.ParticipantStatus
}
export type EnumParticipantStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.ParticipantStatus | Prisma.EnumParticipantStatusFieldRefInput<$PrismaModel>
in?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumParticipantStatusWithAggregatesFilter<$PrismaModel> | $Enums.ParticipantStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedEnumTripStatusFilter<$PrismaModel = never> = {
equals?: $Enums.TripStatus | Prisma.EnumTripStatusFieldRefInput<$PrismaModel>
in?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumTripStatusFilter<$PrismaModel> | $Enums.TripStatus
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedEnumTripStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.TripStatus | Prisma.EnumTripStatusFieldRefInput<$PrismaModel>
in?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.TripStatus[] | Prisma.ListEnumTripStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumTripStatusWithAggregatesFilter<$PrismaModel> | $Enums.TripStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumTripStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumTripStatusFilter<$PrismaModel>
}
export type NestedEnumParticipantStatusFilter<$PrismaModel = never> = {
equals?: $Enums.ParticipantStatus | Prisma.EnumParticipantStatusFieldRefInput<$PrismaModel>
in?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel> | $Enums.ParticipantStatus
}
export type NestedEnumParticipantStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.ParticipantStatus | Prisma.EnumParticipantStatusFieldRefInput<$PrismaModel>
in?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.ParticipantStatus[] | Prisma.ListEnumParticipantStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumParticipantStatusWithAggregatesFilter<$PrismaModel> | $Enums.ParticipantStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumParticipantStatusFilter<$PrismaModel>
}
+28
View File
@@ -0,0 +1,28 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
export const TripStatus = {
OPEN: 'OPEN',
FULL: 'FULL',
CLOSED: 'CLOSED',
COMPLETED: 'COMPLETED'
} as const
export type TripStatus = (typeof TripStatus)[keyof typeof TripStatus]
export const ParticipantStatus = {
PENDING: 'PENDING',
CONFIRMED: 'CONFIRMED',
CANCELLED: 'CANCELLED'
} as const
export type ParticipantStatus = (typeof ParticipantStatus)[keyof typeof ParticipantStatus]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,986 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "../models"
import { type PrismaClient } from "./class"
export type * from '../models'
export type DMMF = typeof runtime.DMMF
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
/**
* Prisma Errors
*/
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
export const PrismaClientValidationError = runtime.PrismaClientValidationError
export type PrismaClientValidationError = runtime.PrismaClientValidationError
/**
* Re-export of sql-template-tag
*/
export const sql = runtime.sqltag
export const empty = runtime.empty
export const join = runtime.join
export const raw = runtime.raw
export const Sql = runtime.Sql
export type Sql = runtime.Sql
/**
* Decimal.js
*/
export const Decimal = runtime.Decimal
export type Decimal = runtime.Decimal
export type DecimalJsLike = runtime.DecimalJsLike
/**
* Extensions
*/
export type Extension = runtime.Types.Extensions.UserArgs
export const getExtensionContext = runtime.Extensions.getExtensionContext
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
export type PrismaVersion = {
client: string
engine: string
}
/**
* Prisma Client JS version: 7.7.0
* Query Engine version: 75cbdc1eb7150937890ad5465d861175c6624711
*/
export const prismaVersion: PrismaVersion = {
client: "7.7.0",
engine: "75cbdc1eb7150937890ad5465d861175c6624711"
}
/**
* Utility Types
*/
export type Bytes = runtime.Bytes
export type JsonObject = runtime.JsonObject
export type JsonArray = runtime.JsonArray
export type JsonValue = runtime.JsonValue
export type InputJsonObject = runtime.InputJsonObject
export type InputJsonArray = runtime.InputJsonArray
export type InputJsonValue = runtime.InputJsonValue
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
type SelectAndInclude = {
select: any
include: any
}
type SelectAndOmit = {
select: any
omit: any
}
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Prisma__Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
export type Enumerable<T> = T | Array<T>;
/**
* Subset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
*/
export type Subset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
};
/**
* SelectSubset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
* Additionally, it validates, if both select and include are present. If the case, it errors.
*/
export type SelectSubset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
(T extends SelectAndInclude
? 'Please either choose `select` or `include`.'
: T extends SelectAndOmit
? 'Please either choose `select` or `omit`.'
: {})
/**
* Subset + Intersection
* @desc From `T` pick properties that exist in `U` and intersect `K`
*/
export type SubsetIntersection<T, U, K> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
K
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/**
* XOR is needed to have a real mutually exclusive union type
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
*/
export type XOR<T, U> =
T extends object ?
U extends object ?
(Without<T, U> & U) | (Without<U, T> & T)
: U : T
/**
* Is T a Record?
*/
type IsObject<T extends any> = T extends Array<any>
? False
: T extends Date
? False
: T extends Uint8Array
? False
: T extends BigInt
? False
: T extends object
? True
: False
/**
* If it's T[], return T
*/
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
/**
* From ts-toolbelt
*/
type __Either<O extends object, K extends Key> = Omit<O, K> &
{
// Merge all but K
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
}[K]
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
type _Either<
O extends object,
K extends Key,
strict extends Boolean
> = {
1: EitherStrict<O, K>
0: EitherLoose<O, K>
}[strict]
export type Either<
O extends object,
K extends Key,
strict extends Boolean = 1
> = O extends unknown ? _Either<O, K, strict> : never
export type Union = any
export type PatchUndefined<O extends object, O1 extends object> = {
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
} & {}
/** Helper Types for "Merge" **/
export type IntersectOf<U extends Union> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
export type Overwrite<O extends object, O1 extends object> = {
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
} & {};
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
[K in keyof U]-?: At<U, K>;
}>>;
type Key = string | number | symbol;
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
1: AtStrict<O, K>;
0: AtLoose<O, K>;
}[strict];
export type ComputeRaw<A extends any> = A extends Function ? A : {
[K in keyof A]: A[K];
} & {};
export type OptionalFlat<O> = {
[K in keyof O]?: O[K];
} & {};
type _Record<K extends keyof any, T> = {
[P in K]: T;
};
// cause typescript not to expand types and preserve names
type NoExpand<T> = T extends unknown ? T : never;
// this type assumes the passed object is entirely optional
export type AtLeast<O extends object, K extends string> = NoExpand<
O extends unknown
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
: never>;
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
/** End Helper Types for "Merge" **/
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
export type Boolean = True | False
export type True = 1
export type False = 0
export type Not<B extends Boolean> = {
0: 1
1: 0
}[B]
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
? 0 // anything `never` is false
: A1 extends A2
? 1
: 0
export type Has<U extends Union, U1 extends Union> = Not<
Extends<Exclude<U1, U>, U1>
>
export type Or<B1 extends Boolean, B2 extends Boolean> = {
0: {
0: 0
1: 1
}
1: {
0: 1
1: 1
}
}[B1][B2]
export type Keys<U extends Union> = U extends unknown ? keyof U : never
export type GetScalarType<T, O> = O extends object ? {
[P in keyof T]: P extends keyof O
? O[P]
: never
} : never
type FieldPaths<
T,
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
> = IsObject<T> extends True ? U : T
export type GetHavingFields<T> = {
[K in keyof T]: Or<
Or<Extends<'OR', K>, Extends<'AND', K>>,
Extends<'NOT', K>
> extends True
? // infer is only needed to not hit TS limit
// based on the brilliant idea of Pierre-Antoine Mills
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
T[K] extends infer TK
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
: never
: {} extends FieldPaths<T[K]>
? never
: K
}[keyof T]
/**
* Convert tuple to union
*/
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
/**
* Like `Pick`, but additionally can also accept an array of keys
*/
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
/**
* Exclude all keys with underscores
*/
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
export const ModelName = {
User: 'User',
Trip: 'Trip',
TripParticipant: 'TripParticipant'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
}
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
globalOmitOptions: {
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "trip" | "tripParticipant"
txIsolationLevel: TransactionIsolationLevel
}
model: {
User: {
payload: Prisma.$UserPayload<ExtArgs>
fields: Prisma.UserFieldRefs
operations: {
findUnique: {
args: Prisma.UserFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findUniqueOrThrow: {
args: Prisma.UserFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findFirst: {
args: Prisma.UserFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findFirstOrThrow: {
args: Prisma.UserFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findMany: {
args: Prisma.UserFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
create: {
args: Prisma.UserCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
createMany: {
args: Prisma.UserCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.UserCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
delete: {
args: Prisma.UserDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
update: {
args: Prisma.UserUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
deleteMany: {
args: Prisma.UserDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.UserUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.UserUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
upsert: {
args: Prisma.UserUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
aggregate: {
args: Prisma.UserAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser>
}
groupBy: {
args: Prisma.UserGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserGroupByOutputType>[]
}
count: {
args: Prisma.UserCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserCountAggregateOutputType> | number
}
}
}
Trip: {
payload: Prisma.$TripPayload<ExtArgs>
fields: Prisma.TripFieldRefs
operations: {
findUnique: {
args: Prisma.TripFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload> | null
}
findUniqueOrThrow: {
args: Prisma.TripFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
findFirst: {
args: Prisma.TripFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload> | null
}
findFirstOrThrow: {
args: Prisma.TripFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
findMany: {
args: Prisma.TripFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>[]
}
create: {
args: Prisma.TripCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
createMany: {
args: Prisma.TripCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.TripCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>[]
}
delete: {
args: Prisma.TripDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
update: {
args: Prisma.TripUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
deleteMany: {
args: Prisma.TripDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.TripUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.TripUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>[]
}
upsert: {
args: Prisma.TripUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripPayload>
}
aggregate: {
args: Prisma.TripAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateTrip>
}
groupBy: {
args: Prisma.TripGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.TripGroupByOutputType>[]
}
count: {
args: Prisma.TripCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.TripCountAggregateOutputType> | number
}
}
}
TripParticipant: {
payload: Prisma.$TripParticipantPayload<ExtArgs>
fields: Prisma.TripParticipantFieldRefs
operations: {
findUnique: {
args: Prisma.TripParticipantFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload> | null
}
findUniqueOrThrow: {
args: Prisma.TripParticipantFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
findFirst: {
args: Prisma.TripParticipantFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload> | null
}
findFirstOrThrow: {
args: Prisma.TripParticipantFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
findMany: {
args: Prisma.TripParticipantFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>[]
}
create: {
args: Prisma.TripParticipantCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
createMany: {
args: Prisma.TripParticipantCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.TripParticipantCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>[]
}
delete: {
args: Prisma.TripParticipantDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
update: {
args: Prisma.TripParticipantUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
deleteMany: {
args: Prisma.TripParticipantDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.TripParticipantUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.TripParticipantUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>[]
}
upsert: {
args: Prisma.TripParticipantUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripParticipantPayload>
}
aggregate: {
args: Prisma.TripParticipantAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateTripParticipant>
}
groupBy: {
args: Prisma.TripParticipantGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.TripParticipantGroupByOutputType>[]
}
count: {
args: Prisma.TripParticipantCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.TripParticipantCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
payload: any
operations: {
$executeRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$executeRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
$queryRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$queryRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
}
}
}
/**
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
password: 'password',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const TripScalarFieldEnum = {
id: 'id',
title: 'title',
description: 'description',
mountain: 'mountain',
location: 'location',
date: 'date',
maxParticipants: 'maxParticipants',
price: 'price',
image: 'image',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
organizerId: 'organizerId'
} as const
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
export const TripParticipantScalarFieldEnum = {
id: 'id',
status: 'status',
createdAt: 'createdAt',
tripId: 'tripId',
userId: 'userId'
} as const
export type TripParticipantScalarFieldEnum = (typeof TripParticipantScalarFieldEnum)[keyof typeof TripParticipantScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
/**
* Field references
*/
/**
* Reference to a field of type 'String'
*/
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
/**
* Reference to a field of type 'String[]'
*/
export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'>
/**
* Reference to a field of type 'DateTime'
*/
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
/**
* Reference to a field of type 'DateTime[]'
*/
export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'>
/**
* Reference to a field of type 'Int'
*/
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
/**
* Reference to a field of type 'Int[]'
*/
export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'>
/**
* Reference to a field of type 'TripStatus'
*/
export type EnumTripStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'TripStatus'>
/**
* Reference to a field of type 'TripStatus[]'
*/
export type ListEnumTripStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'TripStatus[]'>
/**
* Reference to a field of type 'ParticipantStatus'
*/
export type EnumParticipantStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'ParticipantStatus'>
/**
* Reference to a field of type 'ParticipantStatus[]'
*/
export type ListEnumParticipantStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'ParticipantStatus[]'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/**
* Batch Payload for updateMany & deleteMany & createMany
*/
export type BatchPayload = {
count: number
}
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
export type DefaultPrismaClient = PrismaClient
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
export type PrismaClientOptions = ({
/**
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
*/
adapter: runtime.SqlDriverAdapterFactory
accelerateUrl?: never
} | {
/**
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
*/
accelerateUrl: string
adapter?: never
}) & {
/**
* @default "colorless"
*/
errorFormat?: ErrorFormat
/**
* @example
* ```
* // Shorthand for `emit: 'stdout'`
* log: ['query', 'info', 'warn', 'error']
*
* // Emit as events only
* log: [
* { emit: 'event', level: 'query' },
* { emit: 'event', level: 'info' },
* { emit: 'event', level: 'warn' }
* { emit: 'event', level: 'error' }
* ]
*
* / Emit as events and log to stdout
* og: [
* { emit: 'stdout', level: 'query' },
* { emit: 'stdout', level: 'info' },
* { emit: 'stdout', level: 'warn' }
* { emit: 'stdout', level: 'error' }
*
* ```
* Read more in our [docs](https://pris.ly/d/logging).
*/
log?: (LogLevel | LogDefinition)[]
/**
* The default values for transactionOptions
* maxWait ?= 2000
* timeout ?= 5000
*/
transactionOptions?: {
maxWait?: number
timeout?: number
isolationLevel?: TransactionIsolationLevel
}
/**
* Global configuration for omitting model fields by default.
*
* @example
* ```
* const prisma = new PrismaClient({
* omit: {
* user: {
* password: true
* }
* }
* })
* ```
*/
omit?: GlobalOmitConfig
/**
* SQL commenter plugins that add metadata to SQL queries as comments.
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* comments: [
* traceContext(),
* queryInsights(),
* ],
* })
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
trip?: Prisma.TripOmit
tripParticipant?: Prisma.TripParticipantOmit
}
/* Types for Logging */
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
export type LogDefinition = {
level: LogLevel
emit: 'stdout' | 'event'
}
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetLogType<T> = CheckIsLogLevel<
T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = {
timestamp: Date
query: string
params: string
duration: number
target: string
}
export type LogEvent = {
timestamp: Date
message: string
target: string
}
/* End Types for Logging */
export type PrismaAction =
| 'findUnique'
| 'findUniqueOrThrow'
| 'findMany'
| 'findFirst'
| 'findFirstOrThrow'
| 'create'
| 'createMany'
| 'createManyAndReturn'
| 'update'
| 'updateMany'
| 'updateManyAndReturn'
| 'upsert'
| 'delete'
| 'deleteMany'
| 'executeRaw'
| 'queryRaw'
| 'aggregate'
| 'count'
| 'runCommandRaw'
| 'findRaw'
| 'groupBy'
/**
* `PrismaClient` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>
@@ -0,0 +1,140 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models'
export type * from './prismaNamespace'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Trip: 'Trip',
TripParticipant: 'TripParticipant'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
password: 'password',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const TripScalarFieldEnum = {
id: 'id',
title: 'title',
description: 'description',
mountain: 'mountain',
location: 'location',
date: 'date',
maxParticipants: 'maxParticipants',
price: 'price',
image: 'image',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
organizerId: 'organizerId'
} as const
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
export const TripParticipantScalarFieldEnum = {
id: 'id',
status: 'status',
createdAt: 'createdAt',
tripId: 'tripId',
userId: 'userId'
} as const
export type TripParticipantScalarFieldEnum = (typeof TripParticipantScalarFieldEnum)[keyof typeof TripParticipantScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
+14
View File
@@ -0,0 +1,14 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/User'
export type * from './models/Trip'
export type * from './models/TripParticipant'
export type * from './commonInputTypes'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+50 -15
View File
@@ -1,26 +1,61 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Primary — Hijau Gunung #16A34A */
--color-primary-50: #f0fdf4;
--color-primary-100: #dcfce7;
--color-primary-200: #bbf7d0;
--color-primary-300: #86efac;
--color-primary-400: #4ade80;
--color-primary-500: #22c55e;
--color-primary-600: #16a34a;
--color-primary-700: #15803d;
--color-primary-800: #166534;
--color-primary-900: #14532d;
/* Secondary — Biru Langit #0EA5E9 */
--color-secondary-50: #f0f9ff;
--color-secondary-100: #e0f2fe;
--color-secondary-200: #bae6fd;
--color-secondary-300: #7dd3fc;
--color-secondary-400: #38bdf8;
--color-secondary-500: #0ea5e9;
--color-secondary-600: #0284c7;
--color-secondary-700: #0369a1;
--color-secondary-800: #075985;
--color-secondary-900: #0c4a6e;
/* Neutral — Abu Gelap #1F2937 */
--color-neutral-50: #f9fafb;
--color-neutral-100: #f3f4f6;
--color-neutral-200: #e5e7eb;
--color-neutral-300: #d1d5db;
--color-neutral-400: #9ca3af;
--color-neutral-500: #6b7280;
--color-neutral-600: #4b5563;
--color-neutral-700: #374151;
--color-neutral-800: #1f2937;
--color-neutral-900: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
background-color: #f9fafb;
color: #1f2937;
font-family: var(--font-sans), system-ui, sans-serif;
}
/* Focus ring global */
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #16a34a;
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.15);
}
+12 -4
View File
@@ -1,5 +1,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 "./globals.css";
const geistSans = Geist({
@@ -13,8 +15,9 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "SeTrip",
description:
"Cari open trip pendakian gunung, gabung bareng, nikmati petualangan ke gunung-gunung Jawa Barat.",
};
export default function RootLayout({
@@ -24,10 +27,15 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
lang="id"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="flex min-h-full flex-col bg-neutral-50">
<SessionProvider>
<Navbar />
<main className="flex-1">{children}</main>
</SessionProvider>
</body>
</html>
);
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function LoginPage() {
const router = useRouter();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
setLoading(false);
if (result?.error) {
setError(result.error);
} else {
router.push("/");
router.refresh();
}
}
return (
<div className="flex min-h-[calc(100vh-3.5rem)] items-center justify-center px-4 py-12">
<div className="w-full max-w-sm">
{/* Header */}
<div className="mb-8 text-center">
<Link href="/" className="mb-4 inline-block text-2xl font-bold text-neutral-800">
Se<span className="text-primary-600">Trip</span>
</Link>
<p className="text-sm text-neutral-500">
Login dan mulai petualangan ke gunung
</p>
</div>
{/* Card */}
<div className="rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="email@example.com"
/>
</div>
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="Minimal 6 karakter"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-xl bg-primary-600 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Loading..." : "Login"}
</button>
</form>
</div>
<p className="mt-5 text-center text-sm text-neutral-500">
Belum punya akun?{" "}
<Link href="/register" className="font-semibold text-primary-600 hover:text-primary-700">
Daftar sekarang
</Link>
</p>
</div>
</div>
);
}
+233 -56
View File
@@ -1,65 +1,242 @@
import Image from "next/image";
import Link from "next/link";
import { tripService } from "@/server/services/trip.service";
import { TripCard } from "@/features/trip/components/trip-card";
import { SearchBar } from "@/features/trip/components/search-bar";
export default async function HomePage() {
const trips = await tripService.getOpenTrips();
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const upcomingTrips = trips.filter((t) => new Date(t.date) <= nextWeek);
const budgetTrips = trips.filter((t) => t.price <= 300000).slice(0, 3);
const latestTrips = trips.slice(0, 6);
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
<div className="relative min-h-screen bg-neutral-50">
{/* ========== HERO ========== */}
<section className="relative overflow-hidden bg-neutral-900">
{/* Gradient overlay */}
<div className="absolute inset-0 bg-linear-to-br from-primary-900/90 via-neutral-900/80 to-secondary-900/70" />
<div className="relative mx-auto max-w-4xl px-4 pb-16 pt-14 text-center">
{/* Brand */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-primary-400/30 bg-primary-600/20 px-4 py-1.5">
<span className="text-sm">🏔</span>
<span className="text-sm font-medium text-primary-300">
Open Trip Pendakian Gunung
</span>
</div>
<h1 className="mb-4 text-4xl font-extrabold leading-tight tracking-tight text-white sm:text-5xl">
Se<span className="text-primary-400">Trip</span>
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<p className="mx-auto mb-3 max-w-lg text-lg font-medium text-neutral-300">
Masa cowok sejati, cewek seimut nggak{" "}
<span className="text-primary-400">SeTrip</span> bareng?
</p>
<p className="mx-auto mb-8 max-w-md text-neutral-400">
Yuk mulai dari sini. Cari open trip pendakian, gabung bareng, nikmati
petualangan ke gunung-gunung Jawa Barat.
</p>
<SearchBar />
{/* Stats */}
<div className="mt-10 flex justify-center gap-8 sm:gap-12">
<div>
<p className="text-2xl font-bold text-primary-400">
{trips.length}
</p>
<p className="text-xs text-neutral-400">Trip Tersedia</p>
</div>
<div className="h-10 w-px bg-neutral-700" />
<div>
<p className="text-2xl font-bold text-secondary-400">8</p>
<p className="text-xs text-neutral-400">Gunung Jabar</p>
</div>
<div className="h-10 w-px bg-neutral-700" />
<div>
<p className="text-2xl font-bold text-white">100%</p>
<p className="text-xs text-neutral-400">Seru</p>
</div>
</div>
</div>
</section>
{/* ========== CONTENT ========== */}
<div className="mx-auto max-w-6xl px-4 py-10 space-y-12">
{/* Trip Terdekat */}
{upcomingTrips.length > 0 && (
<section>
<div className="mb-5 flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-100 text-lg">
🔥
</div>
<div>
<h2 className="text-lg font-bold text-neutral-800">
Trip Terdekat
</h2>
<p className="text-xs text-neutral-500">
Berangkat dalam 7 hari ke depan
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{upcomingTrips.slice(0, 3).map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
/>
))}
</div>
</section>
)}
{/* Open Trip */}
<section>
<div className="mb-5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-secondary-100 text-lg">
🏔
</div>
<div>
<h2 className="text-lg font-bold text-neutral-800">
Open Trip
</h2>
<p className="text-xs text-neutral-500">
Pendakian gunung bareng teman baru
</p>
</div>
</div>
<Link
href="/trips"
className="rounded-lg bg-secondary-50 px-3 py-1.5 text-sm font-medium text-secondary-600 hover:bg-secondary-100"
>
Lihat semua
</Link>
</div>
{latestTrips.length === 0 ? (
<div className="rounded-2xl border-2 border-dashed border-neutral-200 bg-white p-14 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-50 text-3xl">
🏕
</div>
<p className="mb-1 text-lg font-bold text-neutral-800">
Belum ada trip tersedia
</p>
<p className="mb-6 text-sm text-neutral-500">
Jadilah yang pertama buat open trip pendakian!
</p>
<Link
href="/create-trip"
className="inline-block rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 hover:bg-primary-700"
>
+ Buat Trip Baru
</Link>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{latestTrips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
/>
))}
</div>
)}
</section>
{/* Budget Friendly */}
{budgetTrips.length > 0 && (
<section>
<div className="mb-5 flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-100 text-lg">
💸
</div>
<div>
<h2 className="text-lg font-bold text-neutral-800">
Budget Friendly
</h2>
<p className="text-xs text-neutral-500">
Trip di bawah Rp 300.000
</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{budgetTrips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
/>
))}
</div>
</section>
)}
{/* CTA Bottom */}
<section className="overflow-hidden rounded-2xl bg-neutral-800 p-8 text-center sm:p-12">
<h2 className="mb-2 text-2xl font-bold text-white">
Siap naik gunung?
</h2>
<p className="mx-auto mb-6 max-w-sm text-neutral-400">
Buat trip sendiri atau gabung trip yang sudah ada. Seru bareng teman
baru!
</p>
<div className="flex justify-center gap-3">
<Link
href="/create-trip"
className="rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 hover:bg-primary-500"
>
Buat Trip
</Link>
<Link
href="/trips"
className="rounded-xl border border-neutral-600 px-6 py-2.5 text-sm font-semibold text-neutral-300 hover:border-neutral-500 hover:text-white"
>
Cari Trip
</Link>
</div>
</section>
</div>
{/* ========== FAB ========== */}
<Link
href="/create-trip"
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary-600 text-2xl font-bold text-white shadow-xl shadow-primary-600/30 transition-all hover:scale-110 hover:bg-primary-500 active:scale-95"
title="Buat Trip"
>
+
</Link>
</div>
);
}
+141
View File
@@ -0,0 +1,141 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { registerAction } from "@/features/auth/actions";
export default function RegisterPage() {
const router = useRouter();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);
const formData = new FormData(e.currentTarget);
const result = await registerAction(formData);
if (result.error) {
setError(result.error);
setLoading(false);
return;
}
const loginResult = await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirect: false,
});
setLoading(false);
if (loginResult?.error) {
router.push("/login");
} else {
router.push("/");
router.refresh();
}
}
return (
<div className="flex min-h-[calc(100vh-3.5rem)] items-center justify-center px-4 py-12">
<div className="w-full max-w-sm">
{/* Header */}
<div className="mb-8 text-center">
<Link href="/" className="mb-4 inline-block text-2xl font-bold text-neutral-800">
Se<span className="text-primary-600">Trip</span>
</Link>
<p className="text-sm text-neutral-500">
Daftar dan mulai cari trip pendakian
</p>
</div>
{/* Card */}
<div className="rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nama Lengkap
</label>
<input
id="name"
name="name"
type="text"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="Nama kamu"
/>
</div>
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="email@example.com"
/>
</div>
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="Minimal 6 karakter"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="mb-1.5 block text-sm font-semibold text-neutral-700">
Konfirmasi Password
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
required
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white"
placeholder="Ulangi password"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-xl bg-primary-600 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Loading..." : "Daftar"}
</button>
</form>
</div>
<p className="mt-5 text-center text-sm text-neutral-500">
Sudah punya akun?{" "}
<Link href="/login" className="font-semibold text-primary-600 hover:text-primary-700">
Login
</Link>
</p>
</div>
</div>
);
}
+215
View File
@@ -0,0 +1,215 @@
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import Link from "next/link";
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";
export default async function TripDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const session = await getServerSession(authOptions);
let trip;
try {
trip = await tripService.getTripById(id);
} catch {
notFound();
}
const activeParticipants = trip.participants.filter(
(p) => p.status !== "CANCELLED"
);
const participantCount = activeParticipants.length;
const spotsLeft = trip.maxParticipants - participantCount;
const fillPercent = Math.min(
(participantCount / trip.maxParticipants) * 100,
100
);
const isOrganizer = session?.user?.id === trip.organizerId;
const currentParticipation = session?.user
? trip.participants.find(
(p) => p.userId === session.user.id && p.status !== "CANCELLED"
)
: null;
return (
<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb */}
<div className="mb-4 flex items-center gap-2 text-sm text-neutral-500">
<Link href="/trips" className="hover:text-primary-600">
Open Trip
</Link>
<span>/</span>
<span className="text-neutral-700">{trip.mountain}</span>
</div>
<div className="overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm">
{/* Header — dark theme */}
<div className="relative overflow-hidden bg-neutral-800 px-6 py-8">
<div className="absolute inset-0 bg-linear-to-br from-primary-900/40 to-secondary-900/30" />
<div className="relative">
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-white">{trip.title}</h1>
<p className="mt-1 flex items-center gap-1.5 text-neutral-300">
<span>🏔</span> {trip.mountain}
</p>
</div>
<span
className={`rounded-full px-3 py-1 text-xs font-bold ${
trip.status === "OPEN"
? "bg-primary-500/20 text-primary-300 ring-1 ring-primary-400/30"
: trip.status === "FULL"
? "bg-amber-500/20 text-amber-300 ring-1 ring-amber-400/30"
: "bg-neutral-500/20 text-neutral-400 ring-1 ring-neutral-500/30"
}`}
>
{trip.status}
</span>
</div>
</div>
</div>
<div className="p-6 space-y-6">
{/* Info Grid */}
<div className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-secondary-100 text-lg">
📍
</span>
<div>
<p className="text-xs font-medium text-neutral-400">Lokasi</p>
<p className="font-semibold text-neutral-800">{trip.location}</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-secondary-100 text-lg">
📅
</span>
<div>
<p className="text-xs font-medium text-neutral-400">Tanggal</p>
<p className="font-semibold text-neutral-800">
{formatDate(trip.date)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-100 text-lg">
💰
</span>
<div>
<p className="text-xs font-medium text-primary-600">Harga</p>
<p className="text-xl font-bold text-primary-700">
{formatRupiah(trip.price)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl bg-neutral-50 p-4">
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-200 text-lg">
👤
</span>
<div>
<p className="text-xs font-medium text-neutral-400">
Organizer
</p>
<p className="font-semibold text-neutral-800">
{trip.organizer.name}
</p>
</div>
</div>
</div>
{/* Participant Progress */}
<div className="rounded-xl border border-neutral-200 p-4">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-semibold text-neutral-700">
Peserta
</span>
<span className="text-sm font-bold text-neutral-800">
{participantCount}{" "}
<span className="font-normal text-neutral-400">
/ {trip.maxParticipants}
</span>
</span>
</div>
<div className="h-2.5 overflow-hidden rounded-full bg-neutral-100">
<div
className={`h-full rounded-full transition-all ${
fillPercent >= 100
? "bg-amber-500"
: fillPercent >= 70
? "bg-secondary-500"
: "bg-primary-500"
}`}
style={{ width: `${fillPercent}%` }}
/>
</div>
<p className="mt-1.5 text-xs text-neutral-500">
{spotsLeft > 0
? `${spotsLeft} slot tersisa — yuk gabung!`
: "Trip sudah penuh"}
</p>
</div>
{/* Description */}
{trip.description && (
<div>
<h2 className="mb-2 text-sm font-bold text-neutral-700">
Deskripsi Trip
</h2>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-neutral-600">
{trip.description}
</p>
</div>
)}
{/* Action */}
<JoinTripButton
tripId={trip.id}
isLoggedIn={!!session?.user}
isOrganizer={isOrganizer}
isJoined={!!currentParticipation}
isFull={spotsLeft <= 0}
tripStatus={trip.status}
/>
{/* Participants List */}
<div>
<h2 className="mb-3 text-sm font-bold text-neutral-700">
Peserta ({participantCount})
</h2>
{participantCount === 0 ? (
<p className="text-sm text-neutral-400">
Belum ada peserta. Jadilah yang pertama! 🎒
</p>
) : (
<div className="flex flex-wrap gap-2">
{activeParticipants.map((p) => (
<div
key={p.id}
className="flex items-center gap-2 rounded-full bg-neutral-100 px-3 py-1.5"
>
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary-600 text-[10px] font-bold text-white">
{p.user.name.charAt(0).toUpperCase()}
</div>
<span className="text-sm font-medium text-neutral-700">
{p.user.name}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
import Link from "next/link";
import { tripService } from "@/server/services/trip.service";
import { TripCard } from "@/features/trip/components/trip-card";
export default async function TripsPage() {
const trips = await tripService.getOpenTrips();
return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-neutral-800">
Open Trip Pendakian
</h1>
<p className="mt-1 text-sm text-neutral-500">
{trips.length} trip tersedia pilih dan langsung join
</p>
</div>
<Link
href="/create-trip"
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-md shadow-primary-600/20 hover:bg-primary-700"
>
+ Buat Trip
</Link>
</div>
{trips.length === 0 ? (
<div className="rounded-2xl border-2 border-dashed border-neutral-200 bg-white p-14 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-50 text-3xl">
🏕
</div>
<p className="mb-1 text-lg font-bold text-neutral-800">
Belum ada trip tersedia
</p>
<p className="mb-6 text-sm text-neutral-500">
Jadilah yang pertama membuat open trip pendakian!
</p>
<Link
href="/create-trip"
className="inline-block rounded-xl bg-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-primary-600/25 hover:bg-primary-700"
>
Buat Trip Baru
</Link>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trips.map((trip) => (
<TripCard
key={trip.id}
id={trip.id}
title={trip.title}
mountain={trip.mountain}
location={trip.location}
date={trip.date}
price={trip.price}
maxParticipants={trip.maxParticipants}
participantCount={trip._count.participants}
organizerName={trip.organizer.name}
status={trip.status}
/>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,7 @@
"use client";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
export function SessionProvider({ children }: { children: React.ReactNode }) {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}
+73
View File
@@ -0,0 +1,73 @@
"use client";
import Link from "next/link";
import { useSession, signOut } from "next-auth/react";
export function Navbar() {
const { data: session } = useSession();
return (
<nav className="sticky top-0 z-40 border-b border-neutral-200 bg-white/90 backdrop-blur-md">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<Link href="/" className="flex items-center gap-1.5">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary-600 text-xs font-bold text-white">
S
</span>
<span className="text-lg font-bold text-neutral-800">
Se<span className="text-primary-600">Trip</span>
</span>
</Link>
<div className="flex items-center gap-1">
<Link
href="/trips"
className="rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-800"
>
Open Trip
</Link>
{session?.user ? (
<>
<Link
href="/create-trip"
className="rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-800"
>
Buat Trip
</Link>
<div className="ml-2 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-600 text-xs font-bold text-white">
{session.user.name?.charAt(0).toUpperCase()}
</div>
<span className="hidden text-sm font-medium text-neutral-700 sm:block">
{session.user.name}
</span>
<button
onClick={() => signOut()}
className="rounded-lg px-2.5 py-1.5 text-sm font-medium text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-700"
>
Logout
</button>
</div>
</>
) : (
<>
<Link
href="/login"
className="rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-800"
>
Login
</Link>
<Link
href="/register"
className="ml-1 rounded-lg bg-primary-600 px-4 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-primary-700"
>
Daftar
</Link>
</>
)}
</div>
</div>
</nav>
);
}
+18
View File
@@ -0,0 +1,18 @@
version: "3.9"
services:
postgres:
image: postgres:15
container_name: setrip-postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_USER: setrip_user
POSTGRES_PASSWORD: setrip_password
POSTGRES_DB: setrip_db
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
+29
View File
@@ -0,0 +1,29 @@
"use server";
import { registerSchema } from "./schemas";
import { authService } from "@/server/services/auth.service";
export async function registerAction(formData: FormData) {
const raw = {
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
confirmPassword: formData.get("confirmPassword") as string,
};
const result = registerSchema.safeParse(raw);
if (!result.success) {
return { error: result.error.issues[0].message };
}
try {
await authService.register({
name: result.data.name,
email: result.data.email,
password: result.data.password,
});
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod/v4";
export const loginSchema = z.object({
email: z.email("Email tidak valid"),
password: z.string().min(6, "Password minimal 6 karakter"),
});
export const registerSchema = z.object({
name: z.string().min(2, "Nama minimal 2 karakter"),
email: z.email("Email tidak valid"),
password: z.string().min(6, "Password minimal 6 karakter"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Password tidak cocok",
path: ["confirmPassword"],
});
export type LoginInput = z.infer<typeof loginSchema>;
export type RegisterInput = z.infer<typeof registerSchema>;
+72
View File
@@ -0,0 +1,72 @@
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { createTripSchema } from "./schemas";
import { tripService } from "@/server/services/trip.service";
import { revalidatePath } from "next/cache";
export async function createTripAction(formData: FormData) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
mountain: formData.get("mountain") as string,
location: formData.get("location") as string,
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);
if (!result.success) {
return { error: result.error.issues[0].message };
}
try {
const trip = await tripService.createTrip({
...result.data,
date: new Date(result.data.date),
organizerId: session.user.id,
});
revalidatePath("/trips");
return { success: true, tripId: trip.id };
} catch (err) {
return { error: (err as Error).message };
}
}
export async function joinTripAction(tripId: string) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
try {
await tripService.joinTrip(tripId, session.user.id);
revalidatePath(`/trips/${tripId}`);
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
export async function cancelJoinAction(tripId: string) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
try {
await tripService.cancelJoin(tripId, session.user.id);
revalidatePath(`/trips/${tripId}`);
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
@@ -0,0 +1,110 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { joinTripAction, cancelJoinAction } from "@/features/trip/actions";
interface JoinTripButtonProps {
tripId: string;
isLoggedIn: boolean;
isOrganizer: boolean;
isJoined: boolean;
isFull: boolean;
tripStatus: string;
}
export function JoinTripButton({
tripId,
isLoggedIn,
isOrganizer,
isJoined,
isFull,
tripStatus,
}: JoinTripButtonProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
if (!isLoggedIn) {
return (
<Link
href="/login"
className="block w-full rounded-xl bg-primary-600 py-3 text-center text-sm font-bold text-white shadow-lg shadow-primary-600/20 hover:bg-primary-700"
>
Login untuk Join Trip
</Link>
);
}
if (isOrganizer) {
return (
<div className="rounded-xl bg-secondary-50 py-3 text-center text-sm font-medium text-secondary-700">
Kamu adalah organizer trip ini
</div>
);
}
if (tripStatus !== "OPEN" && !isJoined) {
return (
<div className="rounded-xl bg-neutral-100 py-3 text-center text-sm font-medium text-neutral-500">
Trip tidak tersedia untuk pendaftaran
</div>
);
}
async function handleJoin() {
setLoading(true);
setError("");
const result = await joinTripAction(tripId);
setLoading(false);
if (result.error) {
setError(result.error);
} else {
router.refresh();
}
}
async function handleCancel() {
setLoading(true);
setError("");
const result = await cancelJoinAction(tripId);
setLoading(false);
if (result.error) {
setError(result.error);
} else {
router.refresh();
}
}
return (
<div>
{error && (
<div className="mb-3 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
{isJoined ? (
<button
onClick={handleCancel}
disabled={loading}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
>
{loading ? "Memproses..." : "Batal Ikut"}
</button>
) : (
<button
onClick={handleJoin}
disabled={loading || isFull}
className="w-full rounded-xl bg-primary-600 py-3 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading
? "Memproses..."
: isFull
? "Trip Sudah Penuh"
: "Join Trip Sekarang"}
</button>
)}
</div>
);
}
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function SearchBar() {
const router = useRouter();
const [query, setQuery] = useState("");
function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/trips?q=${encodeURIComponent(query.trim())}`);
} else {
router.push("/trips");
}
}
return (
<form onSubmit={handleSearch} className="mx-auto max-w-xl">
<div className="flex overflow-hidden rounded-2xl bg-white/10 ring-1 ring-white/20 backdrop-blur-sm transition-all focus-within:bg-white/15 focus-within:ring-primary-400/50">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Cari gunung, lokasi, atau trip..."
className="flex-1 border-none bg-transparent px-5 py-3.5 text-sm text-white outline-none placeholder:text-neutral-400"
/>
<button
type="submit"
className="bg-primary-600 px-6 text-sm font-semibold text-white transition-colors hover:bg-primary-500"
>
Cari
</button>
</div>
</form>
);
}
+84
View File
@@ -0,0 +1,84 @@
import Link from "next/link";
import { formatRupiah, formatDate } from "@/lib/utils";
interface TripCardProps {
id: string;
title: string;
mountain: string;
location: string;
date: Date | string;
price: number;
maxParticipants: number;
participantCount: number;
organizerName: string;
status: string;
}
export function TripCard({
id,
title,
mountain,
location,
date,
price,
maxParticipants,
participantCount,
organizerName,
status,
}: TripCardProps) {
const spotsLeft = maxParticipants - participantCount;
return (
<Link href={`/trips/${id}`} className="group block">
<div className="rounded-2xl border border-neutral-200 bg-white p-5 transition-all group-hover:-translate-y-0.5 group-hover:shadow-lg group-hover:shadow-neutral-200/60">
{/* Header */}
<div className="mb-3 flex items-start justify-between">
<div>
<h3 className="font-bold text-neutral-800 group-hover:text-primary-700">
{title}
</h3>
<p className="mt-0.5 text-sm text-neutral-500">{mountain}</p>
</div>
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${
status === "OPEN"
? "bg-primary-100 text-primary-700"
: status === "FULL"
? "bg-amber-100 text-amber-700"
: "bg-neutral-100 text-neutral-500"
}`}
>
{status}
</span>
</div>
{/* Info */}
<div className="space-y-1.5 text-sm text-neutral-600">
<div className="flex items-center gap-1.5">
<span className="text-secondary-500">📍</span> {location}
</div>
<div className="flex items-center gap-1.5">
<span className="text-secondary-500">📅</span> {formatDate(date)}
</div>
<div className="flex items-center gap-1.5">
<span className="text-secondary-500">👤</span> {organizerName}
</div>
</div>
{/* Footer */}
<div className="mt-4 flex items-center justify-between border-t border-neutral-100 pt-3">
<span className="text-lg font-bold text-primary-600">
{formatRupiah(price)}
</span>
<span
className={`text-xs font-semibold ${
spotsLeft > 0 ? "text-secondary-600" : "text-amber-600"
}`}
>
{spotsLeft > 0 ? `${spotsLeft} slot tersisa` : "Penuh"}
</span>
</div>
</div>
</Link>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { z } from "zod/v4";
export const createTripSchema = z.object({
title: z.string().min(3, "Judul minimal 3 karakter"),
description: z.string().optional(),
mountain: z.string().min(2, "Nama gunung harus diisi"),
location: z.string().min(2, "Lokasi harus diisi"),
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<typeof createTripSchema>;
+66
View File
@@ -0,0 +1,66 @@
import { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Email dan password harus diisi");
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) {
throw new Error("Email tidak ditemukan");
}
const isPasswordValid = await bcrypt.compare(
credentials.password,
user.password
);
if (!isPasswordValid) {
throw new Error("Password salah");
}
return {
id: user.id,
name: user.name,
email: user.email,
image: user.image,
};
},
}),
],
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
},
},
pages: {
signIn: "/login",
},
secret: process.env.NEXTAUTH_SECRET,
};
+19
View File
@@ -0,0 +1,19 @@
import { PrismaClient } from "@/app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createPrismaClient() {
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+19
View File
@@ -0,0 +1,19 @@
import { clsx, type ClassValue } from "clsx";
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}
export function formatRupiah(amount: number): string {
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(amount);
}
export function formatDate(date: Date | string): string {
return new Intl.DateTimeFormat("id-ID", {
dateStyle: "long",
}).format(new Date(date));
}
+2027 -31
View File
File diff suppressed because it is too large Load Diff
+19 -2
View File
@@ -6,21 +6,38 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"seed": "npx tsx prisma/seed.ts"
},
"prisma": {
"seed": "npx tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"axios": "^1.15.0",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"dayjs": "^1.11.20",
"next": "16.2.3",
"next-auth": "^4.24.14",
"pg": "^8.20.0",
"prisma": "^7.7.0",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.3",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
}
+14
View File
@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});
@@ -0,0 +1,63 @@
-- CreateEnum
CREATE TYPE "TripStatus" AS ENUM ('OPEN', 'FULL', 'CLOSED', 'COMPLETED');
-- CreateEnum
CREATE TYPE "ParticipantStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Trip" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"mountain" TEXT NOT NULL,
"location" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"maxParticipants" INTEGER NOT NULL,
"price" INTEGER NOT NULL,
"image" TEXT,
"status" "TripStatus" NOT NULL DEFAULT 'OPEN',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"organizerId" TEXT NOT NULL,
CONSTRAINT "Trip_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TripParticipant" (
"id" TEXT NOT NULL,
"status" "ParticipantStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"tripId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "TripParticipant_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "TripParticipant_tripId_userId_key" ON "TripParticipant"("tripId", "userId");
-- AddForeignKey
ALTER TABLE "Trip" ADD CONSTRAINT "Trip_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TripParticipant" ADD CONSTRAINT "TripParticipant_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "Trip"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TripParticipant" ADD CONSTRAINT "TripParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+68
View File
@@ -0,0 +1,68 @@
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(cuid())
name String
email String @unique
password String
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
trips Trip[]
participations TripParticipant[]
}
model Trip {
id String @id @default(cuid())
title String
description String?
mountain String
location String
date DateTime
maxParticipants Int
price Int
image String?
status TripStatus @default(OPEN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organizerId String
organizer User @relation(fields: [organizerId], references: [id])
participants TripParticipant[]
}
model TripParticipant {
id String @id @default(cuid())
status ParticipantStatus @default(PENDING)
createdAt DateTime @default(now())
tripId String
trip Trip @relation(fields: [tripId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
@@unique([tripId, userId])
}
enum TripStatus {
OPEN
FULL
CLOSED
COMPLETED
}
enum ParticipantStatus {
PENDING
CONFIRMED
CANCELLED
}
+282
View File
@@ -0,0 +1,282 @@
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");
// ==================== 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",
password,
},
});
const organizer2 = await prisma.user.upsert({
where: { email: "rina@setrip.id" },
update: {},
create: {
name: "Rina Explorer",
email: "rina@setrip.id",
password,
},
});
// User biasa (join trip)
const user1 = await prisma.user.upsert({
where: { email: "budi@gmail.com" },
update: {},
create: {
name: "Budi Santoso",
email: "budi@gmail.com",
password,
},
});
const user2 = await prisma.user.upsert({
where: { email: "sari@gmail.com" },
update: {},
create: {
name: "Sari Dewi",
email: "sari@gmail.com",
password,
},
});
const user3 = await prisma.user.upsert({
where: { email: "doni@gmail.com" },
update: {},
create: {
name: "Doni Prasetyo",
email: "doni@gmail.com",
password,
},
});
const user4 = await prisma.user.upsert({
where: { email: "maya@gmail.com" },
update: {},
create: {
name: "Maya Putri",
email: "maya@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(" Password semua: password123\n");
// ==================== TRIPS ====================
const now = new Date();
const trip1 = await prisma.trip.create({
data: {
title: "Open Trip Papandayan Weekend",
description: `Pendakian santai ke Gunung Papandayan, cocok untuk pemula!
📍 Meeting Point: Alun-alun Garut, 05:00 WIB
🎒 Fasilitas: Transport PP, guide, tenda, makan 3x
⚠️ Bawa: Sleeping bag, jaket, headlamp, air 2L
Itinerary:
- Sabtu: Berangkat → Basecamp → Summit → Camp
- Minggu: Sunrise → Turun → Pulang`,
mountain: "Gunung Papandayan",
location: "Garut, Jawa Barat",
date: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), // 3 hari lagi
maxParticipants: 10,
price: 250000,
status: "OPEN",
organizerId: organizer1.id,
},
});
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: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000), // 5 hari lagi
maxParticipants: 8,
price: 350000,
status: "OPEN",
organizerId: organizer1.id,
},
});
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: new Date(now.getTime() + 6 * 24 * 60 * 60 * 1000), // 6 hari lagi
maxParticipants: 12,
price: 280000,
status: "OPEN",
organizerId: organizer2.id,
},
});
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: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000), // 2 hari lagi
maxParticipants: 15,
price: 120000,
status: "OPEN",
organizerId: organizer2.id,
},
});
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: new Date(now.getTime() + 4 * 24 * 60 * 60 * 1000), // 4 hari lagi
maxParticipants: 10,
price: 150000,
status: "OPEN",
organizerId: organizer1.id,
},
});
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: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000), // 10 hari lagi
maxParticipants: 8,
price: 300000,
status: "OPEN",
organizerId: organizer2.id,
},
});
console.log("✅ 6 Trips 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" },
],
});
// 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("🎉 Seed complete!");
}
main()
.catch((e) => {
console.error("❌ Seed failed:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+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;
},
};
+18
View File
@@ -0,0 +1,18 @@
import "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
name: string;
email: string;
image?: string | null;
};
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
}
}