auth, trips and join trips
This commit is contained in:
@@ -1,5 +1,31 @@
|
|||||||
<!-- BEGIN:nextjs-agent-rules -->
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
# This is NOT the Next.js you know
|
# 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.
|
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 -->
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|||||||
+281
@@ -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**
|
||||||
@@ -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
|
- Gunakan Prisma untuk database
|
||||||
- Menghindari spaghetti code
|
- Gunakan Zod untuk validation
|
||||||
- Memudahkan AI / developer kontribusi
|
- 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)**
|
## Output Style
|
||||||
2. **Separation client vs server logic**
|
|
||||||
3. **Colocation (file dekat dengan fitur)**
|
|
||||||
4. **No over-engineering (keep it simple)**
|
|
||||||
|
|
||||||
---
|
- Clean code
|
||||||
|
- Modular
|
||||||
# 🏗️ Project Structure
|
- Readable
|
||||||
|
|
||||||
```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**
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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]
|
||||||
|
|
||||||
@@ -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
@@ -1,26 +1,61 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--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) {
|
html {
|
||||||
:root {
|
scroll-behavior: smooth;
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background-color: #f9fafb;
|
||||||
color: var(--foreground);
|
color: #1f2937;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
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
@@ -1,5 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { SessionProvider } from "@/components/providers/session-provider";
|
||||||
|
import { Navbar } from "@/components/shared/navbar";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -13,8 +15,9 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "SeTrip",
|
||||||
description: "Generated by create next app",
|
description:
|
||||||
|
"Cari open trip pendakian gunung, gabung bareng, nikmati petualangan ke gunung-gunung Jawa Barat.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -24,10 +27,15 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="id"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
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>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+234
-57
@@ -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 (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="relative min-h-screen bg-neutral-50">
|
||||||
<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">
|
{/* ========== HERO ========== */}
|
||||||
<Image
|
<section className="relative overflow-hidden bg-neutral-900">
|
||||||
className="dark:invert"
|
{/* Gradient overlay */}
|
||||||
src="/next.svg"
|
<div className="absolute inset-0 bg-linear-to-br from-primary-900/90 via-neutral-900/80 to-secondary-900/70" />
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
<div className="relative mx-auto max-w-4xl px-4 pb-16 pt-14 text-center">
|
||||||
height={20}
|
{/* Brand */}
|
||||||
priority
|
<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>
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
<span className="text-sm font-medium text-primary-300">
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
Open Trip Pendakian Gunung
|
||||||
To get started, edit the page.tsx file.
|
</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>
|
</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{" "}
|
<p className="mx-auto mb-3 max-w-lg text-lg font-medium text-neutral-300">
|
||||||
<a
|
Masa cowok sejati, cewek seimut nggak{" "}
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<span className="text-primary-400">SeTrip</span> bareng?
|
||||||
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>
|
</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>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
</section>
|
||||||
<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]"
|
{/* ========== CONTENT ========== */}
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<div className="mx-auto max-w-6xl px-4 py-10 space-y-12">
|
||||||
target="_blank"
|
{/* Trip Terdekat */}
|
||||||
rel="noopener noreferrer"
|
{upcomingTrips.length > 0 && (
|
||||||
>
|
<section>
|
||||||
<Image
|
<div className="mb-5 flex items-center gap-3">
|
||||||
className="dark:invert"
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-100 text-lg">
|
||||||
src="/vercel.svg"
|
🔥
|
||||||
alt="Vercel logomark"
|
</div>
|
||||||
width={16}
|
<div>
|
||||||
height={16}
|
<h2 className="text-lg font-bold text-neutral-800">
|
||||||
/>
|
Trip Terdekat
|
||||||
Deploy Now
|
</h2>
|
||||||
</a>
|
<p className="text-xs text-neutral-500">
|
||||||
<a
|
Berangkat dalam 7 hari ke depan
|
||||||
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]"
|
</p>
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
</div>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
>
|
{upcomingTrips.slice(0, 3).map((trip) => (
|
||||||
Documentation
|
<TripCard
|
||||||
</a>
|
key={trip.id}
|
||||||
</div>
|
id={trip.id}
|
||||||
</main>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
Generated
+2027
-31
File diff suppressed because it is too large
Load Diff
+19
-2
@@ -6,21 +6,38 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"seed": "npx tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "npx tsx prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "16.2.3",
|
||||||
|
"next-auth": "^4.24.14",
|
||||||
|
"pg": "^8.20.0",
|
||||||
|
"prisma": "^7.7.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.3",
|
"eslint-config-next": "16.2.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -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();
|
||||||
|
});
|
||||||
@@ -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" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 } });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
Vendored
+18
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user