create review and profile
This commit is contained in:
@@ -1,36 +1,77 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# SeTrip
|
||||||
|
|
||||||
## Getting Started
|
Aplikasi open trip pendakian yang mempertemukan **organizer** (pembuat trip) dengan **peserta** yang ingin naik gunung bareng atau mencari teman trip.
|
||||||
|
|
||||||
First, run the development server:
|
Stack: [Next.js](https://nextjs.org) (App Router), NextAuth, Prisma (PostgreSQL), Tailwind CSS.
|
||||||
|
|
||||||
|
## Alur aplikasi
|
||||||
|
|
||||||
|
### 1. Autentikasi
|
||||||
|
|
||||||
|
- Pengguna baru mendaftar di `/register` (nama, email, password disimpan di database).
|
||||||
|
- Login di `/login` melalui NextAuth; sesi dipakai di server action dan di halaman client (misalnya navbar, form buat trip).
|
||||||
|
|
||||||
|
Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tidak bisa membuat trip atau join.
|
||||||
|
|
||||||
|
### 2. Organizer: membuat trip
|
||||||
|
|
||||||
|
1. Setelah login, organizer membuka **Buat Trip** (`/create-trip`) dari navbar, halaman `/trips`, beranda, atau tombol mengambang (+).
|
||||||
|
2. Halaman form (`app/create-trip/page.tsx`) memvalidasi sesi di client; jika belum login, ditampilkan ajakan login.
|
||||||
|
3. Organizer mengisi judul, gunung, lokasi, deskripsi (opsional), rentang tanggal (DatePicker), maks peserta, harga (format Rupiah), dan URL gambar opsional (`ImageUrlInput`).
|
||||||
|
4. Submit memanggil server action `createTripAction` (`features/trip/actions.ts`):
|
||||||
|
- Memastikan ada sesi.
|
||||||
|
- Mem-parse dan memvalidasi input dengan Zod (`features/trip/schemas.ts`).
|
||||||
|
- `tripService.createTrip` menulis trip baru ke database lewat `tripRepo.create`, menghubungkan `organizerId` ke user yang login, dan menyimpan gambar jika ada.
|
||||||
|
5. Trip baru berstatus **OPEN** (default schema), lalu pengguna diarahkan ke detail trip `/trips/[id]`.
|
||||||
|
|
||||||
|
Organizer **tidak** bisa join trip sendiri; di detail trip tombol join diganti pesan bahwa user adalah organizer.
|
||||||
|
|
||||||
|
### 3. Peserta: mencari trip dan join
|
||||||
|
|
||||||
|
1. **Beranda** (`/`) dan **Open Trip** (`/trips`) menampilkan trip dengan status **OPEN** dan tanggal berangkat tidak di masa lalu (`tripService.getOpenTrips` + filter di repository).
|
||||||
|
2. Filter pencarian (`TripFilter`) mengirim query string; daftar trip disaring di server.
|
||||||
|
3. Dari kartu trip (`TripCard`), pengguna membuka **detail** `/trips/[id]` (`app/trips/[id]/page.tsx`):
|
||||||
|
- Peserta aktif = baris `TripParticipant` yang statusnya bukan `CANCELLED`.
|
||||||
|
- Slot tersisa dan progress bar memakai jumlah peserta aktif tersebut.
|
||||||
|
4. **Join** (`JoinTripButton` + `joinTripAction`):
|
||||||
|
- Jika belum login: tautan ke `/login`.
|
||||||
|
- Jika trip bukan `OPEN` dan user belum join: pendaftaran ditutup (kecuali user sudah terdaftar dan ingin membatalkan, mengikuti logika UI).
|
||||||
|
- `tripService.joinTrip` memeriksa: trip ada, status `OPEN`, bukan organizer, belum terdaftar aktif, kapasitas belum penuh; lalu menambah atau mengaktifkan kembali partisipasi (lihat bagian perbaikan bug di bawah).
|
||||||
|
5. Jika jumlah peserta aktif mencapai `maxParticipants`, status trip diperbarui menjadi **FULL**.
|
||||||
|
6. **Batal ikut** memanggil `cancelJoinAction` → `tripService.cancelJoin`: partisipasi ditandai `CANCELLED`; jika trip sebelumnya `FULL` dan setelah batal slot kosong lagi, status dikembalikan ke **OPEN**.
|
||||||
|
|
||||||
|
### 4. Ringkasan peran data
|
||||||
|
|
||||||
|
| Konsep | Penyimpanan |
|
||||||
|
|--------|-------------|
|
||||||
|
| Trip | Model `Trip` (judul, gunung, lokasi, tanggal, kuota, harga, status, relasi ke organizer) |
|
||||||
|
| Peserta | `TripParticipant` unik per `(tripId, userId)` dengan status `CONFIRMED` / `CANCELLED` (default schema juga mengenal `PENDING`; alur UI saat ini memakai `CONFIRMED` saat join) |
|
||||||
|
|
||||||
|
## Menjalankan secara lokal
|
||||||
|
|
||||||
|
Pastikan PostgreSQL berjalan dan variabel `DATABASE_URL` di `.env` mengarah ke database yang valid.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
npm install
|
||||||
|
npx prisma migrate dev
|
||||||
|
npm run seed # opsional: data contoh
|
||||||
npm run dev
|
npm run dev
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Buka [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
## Perbaikan bug (yang relevan dengan join & listing)
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
1. **Join lagi setelah “Batal ikut”**
|
||||||
|
Satu user hanya boleh satu baris partisipasi per trip (`@@unique([tripId, userId])`). Kode lama mencoba `create` lagi setelah status `CANCELLED`, sehingga bisa gagal dengan pelanggaran unik. Sekarang jika sudah ada baris `CANCELLED`, partisipasi **diaktifkan kembali** (`CONFIRMED`) lewat `participantRepo.reactivate`, bukan insert baru.
|
||||||
|
|
||||||
|
2. **Jumlah peserta di kartu / daftar**
|
||||||
|
`_count.participants` di query listing sebelumnya menghitung semua baris termasuk yang `CANCELLED`, sehingga “slot tersisa” di `TripCard` bisa salah. Count sekarang hanya menghitung peserta dengan status **bukan** `CANCELLED`.
|
||||||
|
|
||||||
|
3. **Segar halaman setelah join/batal/buat trip**
|
||||||
|
Setelah aksi trip, cache halaman `/trips` dan `/` ikut di-`revalidatePath` agar jumlah slot dan daftar di beranda konsisten tanpa harus refresh manual.
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
- [Next.js Documentation](https://nextjs.org/docs)
|
||||||
|
- [Prisma Documentation](https://www.prisma.io/docs)
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import DatePicker from "react-datepicker";
|
|||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { createTripAction } from "@/features/trip/actions";
|
import { createTripAction } from "@/features/trip/actions";
|
||||||
import { ImageUrlInput } from "@/features/trip/components/image-url-input";
|
import { ImageUrlInput } from "@/features/trip/components/image-url-input";
|
||||||
|
import { formatLocalCalendarYmd } from "@/lib/trip-dates";
|
||||||
|
|
||||||
const SAMPLE_MOUNTAINS = [
|
const SAMPLE_MOUNTAINS = [
|
||||||
{ name: "Gunung Papandayan", location: "Garut, Jawa Barat" },
|
{ name: "Gunung Papandayan", location: "Garut, Jawa Barat" },
|
||||||
@@ -72,10 +73,15 @@ export default function CreateTripPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
// Set date values from DatePicker state
|
// Hari kalender lokal → YYYY-MM-DD (bukan toISOString, supaya tidak geser ke UTC)
|
||||||
formData.set("date", startDate.toISOString().split("T")[0]);
|
formData.set("date", formatLocalCalendarYmd(startDate));
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
formData.set("endDate", endDate.toISOString().split("T")[0]);
|
const startYmd = formatLocalCalendarYmd(startDate);
|
||||||
|
const endYmd = formatLocalCalendarYmd(endDate);
|
||||||
|
// Satu hari: tanggal pulang sama dengan berangkat → jangan kirim endDate (trip 1 hari)
|
||||||
|
if (endYmd !== startYmd) {
|
||||||
|
formData.set("endDate", endYmd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Set raw price number
|
// Set raw price number
|
||||||
formData.set("price", parseRupiahInput(priceDisplay));
|
formData.set("price", parseRupiahInput(priceDisplay));
|
||||||
@@ -222,6 +228,12 @@ export default function CreateTripPage() {
|
|||||||
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||||
Tanggal Berangkat — Pulang
|
Tanggal Berangkat — Pulang
|
||||||
</label>
|
</label>
|
||||||
|
<p className="mb-1.5 text-[11px] leading-snug text-neutral-500 sm:text-xs">
|
||||||
|
Pilih satu tanggal untuk trip <span className="font-medium">satu hari</span>
|
||||||
|
. Pilih rentang untuk trip <span className="font-medium">lebih dari satu hari</span>
|
||||||
|
. Tanggal disimpan sebagai hari kalender yang kamu klik; filter Open Trip memakai{" "}
|
||||||
|
<span className="font-medium">UTC</span> yang sama.
|
||||||
|
</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 z-10 -translate-y-1/2 text-neutral-400">
|
<span className="absolute left-3 top-1/2 z-10 -translate-y-1/2 text-neutral-400">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export type User = Prisma.UserModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Trip = Prisma.TripModel
|
export type Trip = Prisma.TripModel
|
||||||
|
/**
|
||||||
|
* Model TripReview
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type TripReview = Prisma.TripReviewModel
|
||||||
/**
|
/**
|
||||||
* Model TripImage
|
* Model TripImage
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ export type User = Prisma.UserModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Trip = Prisma.TripModel
|
export type Trip = Prisma.TripModel
|
||||||
|
/**
|
||||||
|
* Model TripReview
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type TripReview = Prisma.TripReviewModel
|
||||||
/**
|
/**
|
||||||
* Model TripImage
|
* Model TripImage
|
||||||
*
|
*
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -386,6 +386,7 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
|
|||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Trip: 'Trip',
|
Trip: 'Trip',
|
||||||
|
TripReview: 'TripReview',
|
||||||
TripImage: 'TripImage',
|
TripImage: 'TripImage',
|
||||||
TripParticipant: 'TripParticipant'
|
TripParticipant: 'TripParticipant'
|
||||||
} as const
|
} as const
|
||||||
@@ -403,7 +404,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "trip" | "tripImage" | "tripParticipant"
|
modelProps: "user" | "trip" | "tripReview" | "tripImage" | "tripParticipant"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -555,6 +556,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TripReview: {
|
||||||
|
payload: Prisma.$TripReviewPayload<ExtArgs>
|
||||||
|
fields: Prisma.TripReviewFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.TripReviewFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.TripReviewFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.TripReviewFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.TripReviewFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.TripReviewFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.TripReviewCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.TripReviewCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.TripReviewCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.TripReviewDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.TripReviewUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.TripReviewDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.TripReviewUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.TripReviewUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.TripReviewUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$TripReviewPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.TripReviewAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateTripReview>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.TripReviewGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.TripReviewGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.TripReviewCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.TripReviewCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
TripImage: {
|
TripImage: {
|
||||||
payload: Prisma.$TripImagePayload<ExtArgs>
|
payload: Prisma.$TripImagePayload<ExtArgs>
|
||||||
fields: Prisma.TripImageFieldRefs
|
fields: Prisma.TripImageFieldRefs
|
||||||
@@ -774,6 +849,19 @@ export const TripScalarFieldEnum = {
|
|||||||
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
|
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const TripReviewScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
rating: 'rating',
|
||||||
|
comment: 'comment',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
tripId: 'tripId',
|
||||||
|
userId: 'userId'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type TripReviewScalarFieldEnum = (typeof TripReviewScalarFieldEnum)[keyof typeof TripReviewScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const TripImageScalarFieldEnum = {
|
export const TripImageScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
url: 'url',
|
url: 'url',
|
||||||
@@ -1006,6 +1094,7 @@ export type PrismaClientOptions = ({
|
|||||||
export type GlobalOmitConfig = {
|
export type GlobalOmitConfig = {
|
||||||
user?: Prisma.UserOmit
|
user?: Prisma.UserOmit
|
||||||
trip?: Prisma.TripOmit
|
trip?: Prisma.TripOmit
|
||||||
|
tripReview?: Prisma.TripReviewOmit
|
||||||
tripImage?: Prisma.TripImageOmit
|
tripImage?: Prisma.TripImageOmit
|
||||||
tripParticipant?: Prisma.TripParticipantOmit
|
tripParticipant?: Prisma.TripParticipantOmit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const AnyNull = runtime.AnyNull
|
|||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Trip: 'Trip',
|
Trip: 'Trip',
|
||||||
|
TripReview: 'TripReview',
|
||||||
TripImage: 'TripImage',
|
TripImage: 'TripImage',
|
||||||
TripParticipant: 'TripParticipant'
|
TripParticipant: 'TripParticipant'
|
||||||
} as const
|
} as const
|
||||||
@@ -105,6 +106,19 @@ export const TripScalarFieldEnum = {
|
|||||||
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
|
export type TripScalarFieldEnum = (typeof TripScalarFieldEnum)[keyof typeof TripScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const TripReviewScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
rating: 'rating',
|
||||||
|
comment: 'comment',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
tripId: 'tripId',
|
||||||
|
userId: 'userId'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type TripReviewScalarFieldEnum = (typeof TripReviewScalarFieldEnum)[keyof typeof TripReviewScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const TripImageScalarFieldEnum = {
|
export const TripImageScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
url: 'url',
|
url: 'url',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
export type * from './models/User'
|
export type * from './models/User'
|
||||||
export type * from './models/Trip'
|
export type * from './models/Trip'
|
||||||
|
export type * from './models/TripReview'
|
||||||
export type * from './models/TripImage'
|
export type * from './models/TripImage'
|
||||||
export type * from './models/TripParticipant'
|
export type * from './models/TripParticipant'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
@@ -287,6 +287,7 @@ export type TripWhereInput = {
|
|||||||
organizer?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
organizer?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
participants?: Prisma.TripParticipantListRelationFilter
|
participants?: Prisma.TripParticipantListRelationFilter
|
||||||
images?: Prisma.TripImageListRelationFilter
|
images?: Prisma.TripImageListRelationFilter
|
||||||
|
reviews?: Prisma.TripReviewListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripOrderByWithRelationInput = {
|
export type TripOrderByWithRelationInput = {
|
||||||
@@ -306,6 +307,7 @@ export type TripOrderByWithRelationInput = {
|
|||||||
organizer?: Prisma.UserOrderByWithRelationInput
|
organizer?: Prisma.UserOrderByWithRelationInput
|
||||||
participants?: Prisma.TripParticipantOrderByRelationAggregateInput
|
participants?: Prisma.TripParticipantOrderByRelationAggregateInput
|
||||||
images?: Prisma.TripImageOrderByRelationAggregateInput
|
images?: Prisma.TripImageOrderByRelationAggregateInput
|
||||||
|
reviews?: Prisma.TripReviewOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripWhereUniqueInput = Prisma.AtLeast<{
|
export type TripWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -328,6 +330,7 @@ export type TripWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
organizer?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
organizer?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
participants?: Prisma.TripParticipantListRelationFilter
|
participants?: Prisma.TripParticipantListRelationFilter
|
||||||
images?: Prisma.TripImageListRelationFilter
|
images?: Prisma.TripImageListRelationFilter
|
||||||
|
reviews?: Prisma.TripReviewListRelationFilter
|
||||||
}, "id">
|
}, "id">
|
||||||
|
|
||||||
export type TripOrderByWithAggregationInput = {
|
export type TripOrderByWithAggregationInput = {
|
||||||
@@ -386,6 +389,7 @@ export type TripCreateInput = {
|
|||||||
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
||||||
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
||||||
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedCreateInput = {
|
export type TripUncheckedCreateInput = {
|
||||||
@@ -404,6 +408,7 @@ export type TripUncheckedCreateInput = {
|
|||||||
organizerId: string
|
organizerId: string
|
||||||
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
||||||
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUpdateInput = {
|
export type TripUpdateInput = {
|
||||||
@@ -422,6 +427,7 @@ export type TripUpdateInput = {
|
|||||||
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
||||||
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
||||||
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedUpdateInput = {
|
export type TripUncheckedUpdateInput = {
|
||||||
@@ -440,6 +446,7 @@ export type TripUncheckedUpdateInput = {
|
|||||||
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
||||||
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateManyInput = {
|
export type TripCreateManyInput = {
|
||||||
@@ -620,6 +627,20 @@ export type EnumTripStatusFieldUpdateOperationsInput = {
|
|||||||
set?: $Enums.TripStatus
|
set?: $Enums.TripStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TripCreateNestedOneWithoutReviewsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.TripCreateWithoutReviewsInput, Prisma.TripUncheckedCreateWithoutReviewsInput>
|
||||||
|
connectOrCreate?: Prisma.TripCreateOrConnectWithoutReviewsInput
|
||||||
|
connect?: Prisma.TripWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUpdateOneRequiredWithoutReviewsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.TripCreateWithoutReviewsInput, Prisma.TripUncheckedCreateWithoutReviewsInput>
|
||||||
|
connectOrCreate?: Prisma.TripCreateOrConnectWithoutReviewsInput
|
||||||
|
upsert?: Prisma.TripUpsertWithoutReviewsInput
|
||||||
|
connect?: Prisma.TripWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.TripUpdateToOneWithWhereWithoutReviewsInput, Prisma.TripUpdateWithoutReviewsInput>, Prisma.TripUncheckedUpdateWithoutReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type TripCreateNestedOneWithoutImagesInput = {
|
export type TripCreateNestedOneWithoutImagesInput = {
|
||||||
create?: Prisma.XOR<Prisma.TripCreateWithoutImagesInput, Prisma.TripUncheckedCreateWithoutImagesInput>
|
create?: Prisma.XOR<Prisma.TripCreateWithoutImagesInput, Prisma.TripUncheckedCreateWithoutImagesInput>
|
||||||
connectOrCreate?: Prisma.TripCreateOrConnectWithoutImagesInput
|
connectOrCreate?: Prisma.TripCreateOrConnectWithoutImagesInput
|
||||||
@@ -663,6 +684,7 @@ export type TripCreateWithoutOrganizerInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
||||||
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedCreateWithoutOrganizerInput = {
|
export type TripUncheckedCreateWithoutOrganizerInput = {
|
||||||
@@ -680,6 +702,7 @@ export type TripUncheckedCreateWithoutOrganizerInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
||||||
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateOrConnectWithoutOrganizerInput = {
|
export type TripCreateOrConnectWithoutOrganizerInput = {
|
||||||
@@ -727,6 +750,94 @@ export type TripScalarWhereInput = {
|
|||||||
organizerId?: Prisma.StringFilter<"Trip"> | string
|
organizerId?: Prisma.StringFilter<"Trip"> | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TripCreateWithoutReviewsInput = {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
description?: string | null
|
||||||
|
mountain: string
|
||||||
|
location: string
|
||||||
|
date: Date | string
|
||||||
|
endDate?: Date | string | null
|
||||||
|
maxParticipants: number
|
||||||
|
price: number
|
||||||
|
status?: $Enums.TripStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
||||||
|
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
||||||
|
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUncheckedCreateWithoutReviewsInput = {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
description?: string | null
|
||||||
|
mountain: string
|
||||||
|
location: string
|
||||||
|
date: Date | string
|
||||||
|
endDate?: Date | string | null
|
||||||
|
maxParticipants: number
|
||||||
|
price: number
|
||||||
|
status?: $Enums.TripStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
organizerId: string
|
||||||
|
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripCreateOrConnectWithoutReviewsInput = {
|
||||||
|
where: Prisma.TripWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.TripCreateWithoutReviewsInput, Prisma.TripUncheckedCreateWithoutReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUpsertWithoutReviewsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.TripUpdateWithoutReviewsInput, Prisma.TripUncheckedUpdateWithoutReviewsInput>
|
||||||
|
create: Prisma.XOR<Prisma.TripCreateWithoutReviewsInput, Prisma.TripUncheckedCreateWithoutReviewsInput>
|
||||||
|
where?: Prisma.TripWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUpdateToOneWithWhereWithoutReviewsInput = {
|
||||||
|
where?: Prisma.TripWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.TripUpdateWithoutReviewsInput, Prisma.TripUncheckedUpdateWithoutReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUpdateWithoutReviewsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
mountain?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
location?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
date?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
||||||
|
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
||||||
|
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TripUncheckedUpdateWithoutReviewsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
mountain?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
location?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
date?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
export type TripCreateWithoutImagesInput = {
|
export type TripCreateWithoutImagesInput = {
|
||||||
id?: string
|
id?: string
|
||||||
title: string
|
title: string
|
||||||
@@ -742,6 +853,7 @@ export type TripCreateWithoutImagesInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
||||||
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedCreateWithoutImagesInput = {
|
export type TripUncheckedCreateWithoutImagesInput = {
|
||||||
@@ -759,6 +871,7 @@ export type TripUncheckedCreateWithoutImagesInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
organizerId: string
|
organizerId: string
|
||||||
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
participants?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateOrConnectWithoutImagesInput = {
|
export type TripCreateOrConnectWithoutImagesInput = {
|
||||||
@@ -792,6 +905,7 @@ export type TripUpdateWithoutImagesInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
||||||
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedUpdateWithoutImagesInput = {
|
export type TripUncheckedUpdateWithoutImagesInput = {
|
||||||
@@ -809,6 +923,7 @@ export type TripUncheckedUpdateWithoutImagesInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateWithoutParticipantsInput = {
|
export type TripCreateWithoutParticipantsInput = {
|
||||||
@@ -826,6 +941,7 @@ export type TripCreateWithoutParticipantsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
organizer: Prisma.UserCreateNestedOneWithoutTripsInput
|
||||||
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedCreateWithoutParticipantsInput = {
|
export type TripUncheckedCreateWithoutParticipantsInput = {
|
||||||
@@ -843,6 +959,7 @@ export type TripUncheckedCreateWithoutParticipantsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
organizerId: string
|
organizerId: string
|
||||||
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
images?: Prisma.TripImageUncheckedCreateNestedManyWithoutTripInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutTripInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateOrConnectWithoutParticipantsInput = {
|
export type TripCreateOrConnectWithoutParticipantsInput = {
|
||||||
@@ -876,6 +993,7 @@ export type TripUpdateWithoutParticipantsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
organizer?: Prisma.UserUpdateOneRequiredWithoutTripsNestedInput
|
||||||
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedUpdateWithoutParticipantsInput = {
|
export type TripUncheckedUpdateWithoutParticipantsInput = {
|
||||||
@@ -893,6 +1011,7 @@ export type TripUncheckedUpdateWithoutParticipantsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
organizerId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCreateManyOrganizerInput = {
|
export type TripCreateManyOrganizerInput = {
|
||||||
@@ -925,6 +1044,7 @@ export type TripUpdateWithoutOrganizerInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUpdateManyWithoutTripNestedInput
|
||||||
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedUpdateWithoutOrganizerInput = {
|
export type TripUncheckedUpdateWithoutOrganizerInput = {
|
||||||
@@ -942,6 +1062,7 @@ export type TripUncheckedUpdateWithoutOrganizerInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
participants?: Prisma.TripParticipantUncheckedUpdateManyWithoutTripNestedInput
|
||||||
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
images?: Prisma.TripImageUncheckedUpdateManyWithoutTripNestedInput
|
||||||
|
reviews?: Prisma.TripReviewUncheckedUpdateManyWithoutTripNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripUncheckedUpdateManyWithoutOrganizerInput = {
|
export type TripUncheckedUpdateManyWithoutOrganizerInput = {
|
||||||
@@ -967,11 +1088,13 @@ export type TripUncheckedUpdateManyWithoutOrganizerInput = {
|
|||||||
export type TripCountOutputType = {
|
export type TripCountOutputType = {
|
||||||
participants: number
|
participants: number
|
||||||
images: number
|
images: number
|
||||||
|
reviews: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type TripCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
participants?: boolean | TripCountOutputTypeCountParticipantsArgs
|
participants?: boolean | TripCountOutputTypeCountParticipantsArgs
|
||||||
images?: boolean | TripCountOutputTypeCountImagesArgs
|
images?: boolean | TripCountOutputTypeCountImagesArgs
|
||||||
|
reviews?: boolean | TripCountOutputTypeCountReviewsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -998,6 +1121,13 @@ export type TripCountOutputTypeCountImagesArgs<ExtArgs extends runtime.Types.Ext
|
|||||||
where?: Prisma.TripImageWhereInput
|
where?: Prisma.TripImageWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TripCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type TripCountOutputTypeCountReviewsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.TripReviewWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type TripSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type TripSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -1016,6 +1146,7 @@ export type TripSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
||||||
images?: boolean | Prisma.Trip$imagesArgs<ExtArgs>
|
images?: boolean | Prisma.Trip$imagesArgs<ExtArgs>
|
||||||
|
reviews?: boolean | Prisma.Trip$reviewsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.TripCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.TripCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["trip"]>
|
}, ExtArgs["result"]["trip"]>
|
||||||
|
|
||||||
@@ -1074,6 +1205,7 @@ export type TripInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
||||||
images?: boolean | Prisma.Trip$imagesArgs<ExtArgs>
|
images?: boolean | Prisma.Trip$imagesArgs<ExtArgs>
|
||||||
|
reviews?: boolean | Prisma.Trip$reviewsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.TripCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.TripCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type TripIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type TripIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1089,6 +1221,7 @@ export type $TripPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
organizer: Prisma.$UserPayload<ExtArgs>
|
organizer: Prisma.$UserPayload<ExtArgs>
|
||||||
participants: Prisma.$TripParticipantPayload<ExtArgs>[]
|
participants: Prisma.$TripParticipantPayload<ExtArgs>[]
|
||||||
images: Prisma.$TripImagePayload<ExtArgs>[]
|
images: Prisma.$TripImagePayload<ExtArgs>[]
|
||||||
|
reviews: Prisma.$TripReviewPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1501,6 +1634,7 @@ export interface Prisma__TripClient<T, Null = never, ExtArgs extends runtime.Typ
|
|||||||
organizer<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
organizer<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||||
participants<T extends Prisma.Trip$participantsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Trip$participantsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
participants<T extends Prisma.Trip$participantsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Trip$participantsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
images<T extends Prisma.Trip$imagesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Trip$imagesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripImagePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
images<T extends Prisma.Trip$imagesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Trip$imagesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripImagePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
reviews<T extends Prisma.Trip$reviewsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Trip$reviewsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripReviewPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1991,6 +2125,30 @@ export type Trip$imagesArgs<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
distinct?: Prisma.TripImageScalarFieldEnum | Prisma.TripImageScalarFieldEnum[]
|
distinct?: Prisma.TripImageScalarFieldEnum | Prisma.TripImageScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trip.reviews
|
||||||
|
*/
|
||||||
|
export type Trip$reviewsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the TripReview
|
||||||
|
*/
|
||||||
|
select?: Prisma.TripReviewSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the TripReview
|
||||||
|
*/
|
||||||
|
omit?: Prisma.TripReviewOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.TripReviewInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.TripReviewWhereInput
|
||||||
|
orderBy?: Prisma.TripReviewOrderByWithRelationInput | Prisma.TripReviewOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.TripReviewWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.TripReviewScalarFieldEnum | Prisma.TripReviewScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trip without action
|
* Trip without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,7 @@ export type UserWhereInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
trips?: Prisma.TripListRelationFilter
|
trips?: Prisma.TripListRelationFilter
|
||||||
participations?: Prisma.TripParticipantListRelationFilter
|
participations?: Prisma.TripParticipantListRelationFilter
|
||||||
|
tripReviews?: Prisma.TripReviewListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOrderByWithRelationInput = {
|
export type UserOrderByWithRelationInput = {
|
||||||
@@ -212,6 +213,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
trips?: Prisma.TripOrderByRelationAggregateInput
|
trips?: Prisma.TripOrderByRelationAggregateInput
|
||||||
participations?: Prisma.TripParticipantOrderByRelationAggregateInput
|
participations?: Prisma.TripParticipantOrderByRelationAggregateInput
|
||||||
|
tripReviews?: Prisma.TripReviewOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -227,6 +229,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
trips?: Prisma.TripListRelationFilter
|
trips?: Prisma.TripListRelationFilter
|
||||||
participations?: Prisma.TripParticipantListRelationFilter
|
participations?: Prisma.TripParticipantListRelationFilter
|
||||||
|
tripReviews?: Prisma.TripReviewListRelationFilter
|
||||||
}, "id" | "email">
|
}, "id" | "email">
|
||||||
|
|
||||||
export type UserOrderByWithAggregationInput = {
|
export type UserOrderByWithAggregationInput = {
|
||||||
@@ -265,6 +268,7 @@ export type UserCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
||||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateInput = {
|
export type UserUncheckedCreateInput = {
|
||||||
@@ -277,6 +281,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
||||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateInput = {
|
export type UserUpdateInput = {
|
||||||
@@ -289,6 +294,7 @@ export type UserUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
||||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateInput = {
|
export type UserUncheckedUpdateInput = {
|
||||||
@@ -301,6 +307,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
||||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateManyInput = {
|
export type UserCreateManyInput = {
|
||||||
@@ -394,6 +401,20 @@ export type UserUpdateOneRequiredWithoutTripsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutTripsInput, Prisma.UserUpdateWithoutTripsInput>, Prisma.UserUncheckedUpdateWithoutTripsInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutTripsInput, Prisma.UserUpdateWithoutTripsInput>, Prisma.UserUncheckedUpdateWithoutTripsInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutTripReviewsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutTripReviewsInput, Prisma.UserUncheckedCreateWithoutTripReviewsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutTripReviewsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneRequiredWithoutTripReviewsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutTripReviewsInput, Prisma.UserUncheckedCreateWithoutTripReviewsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutTripReviewsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutTripReviewsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutTripReviewsInput, Prisma.UserUpdateWithoutTripReviewsInput>, Prisma.UserUncheckedUpdateWithoutTripReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type UserCreateNestedOneWithoutParticipationsInput = {
|
export type UserCreateNestedOneWithoutParticipationsInput = {
|
||||||
create?: Prisma.XOR<Prisma.UserCreateWithoutParticipationsInput, Prisma.UserUncheckedCreateWithoutParticipationsInput>
|
create?: Prisma.XOR<Prisma.UserCreateWithoutParticipationsInput, Prisma.UserUncheckedCreateWithoutParticipationsInput>
|
||||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutParticipationsInput
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutParticipationsInput
|
||||||
@@ -417,6 +438,7 @@ export type UserCreateWithoutTripsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutTripsInput = {
|
export type UserUncheckedCreateWithoutTripsInput = {
|
||||||
@@ -428,6 +450,7 @@ export type UserUncheckedCreateWithoutTripsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutTripsInput = {
|
export type UserCreateOrConnectWithoutTripsInput = {
|
||||||
@@ -455,6 +478,7 @@ export type UserUpdateWithoutTripsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutTripsInput = {
|
export type UserUncheckedUpdateWithoutTripsInput = {
|
||||||
@@ -466,6 +490,71 @@ export type UserUncheckedUpdateWithoutTripsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutTripReviewsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
image?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
||||||
|
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutTripReviewsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
image?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
||||||
|
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutTripReviewsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutTripReviewsInput, Prisma.UserUncheckedCreateWithoutTripReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutTripReviewsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutTripReviewsInput, Prisma.UserUncheckedUpdateWithoutTripReviewsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutTripReviewsInput, Prisma.UserUncheckedCreateWithoutTripReviewsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutTripReviewsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutTripReviewsInput, Prisma.UserUncheckedUpdateWithoutTripReviewsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutTripReviewsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
||||||
|
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutTripReviewsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
||||||
|
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutParticipationsInput = {
|
export type UserCreateWithoutParticipationsInput = {
|
||||||
@@ -477,6 +566,7 @@ export type UserCreateWithoutParticipationsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
||||||
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutParticipationsInput = {
|
export type UserUncheckedCreateWithoutParticipationsInput = {
|
||||||
@@ -488,6 +578,7 @@ export type UserUncheckedCreateWithoutParticipationsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutParticipationsInput = {
|
export type UserCreateOrConnectWithoutParticipationsInput = {
|
||||||
@@ -515,6 +606,7 @@ export type UserUpdateWithoutParticipationsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutParticipationsInput = {
|
export type UserUncheckedUpdateWithoutParticipationsInput = {
|
||||||
@@ -526,6 +618,7 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -536,11 +629,13 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
|
|||||||
export type UserCountOutputType = {
|
export type UserCountOutputType = {
|
||||||
trips: number
|
trips: number
|
||||||
participations: number
|
participations: number
|
||||||
|
tripReviews: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
trips?: boolean | UserCountOutputTypeCountTripsArgs
|
trips?: boolean | UserCountOutputTypeCountTripsArgs
|
||||||
participations?: boolean | UserCountOutputTypeCountParticipationsArgs
|
participations?: boolean | UserCountOutputTypeCountParticipationsArgs
|
||||||
|
tripReviews?: boolean | UserCountOutputTypeCountTripReviewsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -567,6 +662,13 @@ export type UserCountOutputTypeCountParticipationsArgs<ExtArgs extends runtime.T
|
|||||||
where?: Prisma.TripParticipantWhereInput
|
where?: Prisma.TripParticipantWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountTripReviewsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.TripReviewWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -578,6 +680,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
|
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
|
||||||
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
|
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
|
||||||
|
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@@ -615,6 +718,7 @@ export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = run
|
|||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
|
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
|
||||||
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
|
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
|
||||||
|
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||||
@@ -625,6 +729,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
objects: {
|
objects: {
|
||||||
trips: Prisma.$TripPayload<ExtArgs>[]
|
trips: Prisma.$TripPayload<ExtArgs>[]
|
||||||
participations: Prisma.$TripParticipantPayload<ExtArgs>[]
|
participations: Prisma.$TripParticipantPayload<ExtArgs>[]
|
||||||
|
tripReviews: Prisma.$TripReviewPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1030,6 +1135,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
|
|||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
trips<T extends Prisma.User$tripsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
trips<T extends Prisma.User$tripsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
participations<T extends Prisma.User$participationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$participationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
participations<T extends Prisma.User$participationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$participationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
tripReviews<T extends Prisma.User$tripReviewsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripReviewsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripReviewPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1506,6 +1612,30 @@ export type User$participationsArgs<ExtArgs extends runtime.Types.Extensions.Int
|
|||||||
distinct?: Prisma.TripParticipantScalarFieldEnum | Prisma.TripParticipantScalarFieldEnum[]
|
distinct?: Prisma.TripParticipantScalarFieldEnum | Prisma.TripParticipantScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.tripReviews
|
||||||
|
*/
|
||||||
|
export type User$tripReviewsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the TripReview
|
||||||
|
*/
|
||||||
|
select?: Prisma.TripReviewSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the TripReview
|
||||||
|
*/
|
||||||
|
omit?: Prisma.TripReviewOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.TripReviewInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.TripReviewWhereInput
|
||||||
|
orderBy?: Prisma.TripReviewOrderByWithRelationInput | Prisma.TripReviewOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.TripReviewWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.TripReviewScalarFieldEnum | Prisma.TripReviewScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User without action
|
* User without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
+19
-4
@@ -1,13 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, Suspense } from "react";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function LoginPage() {
|
function safeInternalPath(raw: string | null): string {
|
||||||
|
if (!raw || !raw.startsWith("/") || raw.startsWith("//")) return "/";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -31,7 +37,8 @@ export default function LoginPage() {
|
|||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
setError(result.error);
|
setError(result.error);
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
const next = safeInternalPath(searchParams.get("callbackUrl"));
|
||||||
|
router.push(next);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,3 +133,11 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LoginForm />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { profileService } from "@/server/services/profile.service";
|
||||||
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
|
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||||
|
|
||||||
|
export default async function ProfilePage() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) {
|
||||||
|
redirect("/login?callbackUrl=/profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await profileService.getProfileDashboard(session.user.id);
|
||||||
|
const { user, organizedTrips, activeJoined, cancelledJoined, reviewable } =
|
||||||
|
data;
|
||||||
|
|
||||||
|
const memberSince = new Intl.DateTimeFormat("id-ID", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(user.createdAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-6 sm:py-8">
|
||||||
|
<div className="mb-6 flex flex-col gap-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:flex-row sm:items-center sm:justify-between sm:p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{user.image ? (
|
||||||
|
<Image
|
||||||
|
src={user.image}
|
||||||
|
alt=""
|
||||||
|
width={72}
|
||||||
|
height={72}
|
||||||
|
className="h-[72px] w-[72px] rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center rounded-full bg-primary-600 text-2xl font-bold text-white">
|
||||||
|
{user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="text-xl font-bold text-neutral-800 sm:text-2xl">
|
||||||
|
{user.name}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-0.5 truncate text-sm text-neutral-500">{user.email}</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-400">
|
||||||
|
Anggota sejak {memberSince}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs text-neutral-500">
|
||||||
|
Di sini kamu bisa lihat trip yang kamu buat sebagai{" "}
|
||||||
|
<span className="font-semibold text-primary-700">organizer</span>,
|
||||||
|
trip yang kamu{" "}
|
||||||
|
<span className="font-semibold text-secondary-700">ikuti</span>{" "}
|
||||||
|
sebagai peserta, dan{" "}
|
||||||
|
<span className="font-semibold text-amber-700">ulasan</span> untuk
|
||||||
|
trip yang sudah selesai (lewat halaman trip).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/create-trip"
|
||||||
|
className="shrink-0 rounded-xl bg-primary-600 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-md hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
+ Buat trip
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trip selesai — akses ulasan (trip ini tidak muncul di Open Trip) */}
|
||||||
|
{reviewable.length > 0 && (
|
||||||
|
<section className="mb-8 rounded-2xl border border-amber-200 bg-amber-50/60 p-4 sm:p-5">
|
||||||
|
<h2 className="mb-1 text-base font-bold text-amber-900 sm:text-lg">
|
||||||
|
Trip selesai & ulasan
|
||||||
|
</h2>
|
||||||
|
<p className="mb-4 text-xs text-amber-900/80 sm:text-sm">
|
||||||
|
Trip yang sudah lewat tidak tampil di daftar Open Trip. Buka trip di
|
||||||
|
bawah ini lalu scroll ke bagian ulasan di halaman detail untuk
|
||||||
|
memberi atau mengubah rating.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{reviewable.map((p) => {
|
||||||
|
const t = p.trip;
|
||||||
|
const hasReview = t.reviews.length > 0;
|
||||||
|
return (
|
||||||
|
<li key={p.id}>
|
||||||
|
<ProfileTripRow
|
||||||
|
href={`/trips/${t.id}`}
|
||||||
|
title={t.title}
|
||||||
|
mountain={t.mountain}
|
||||||
|
date={t.date}
|
||||||
|
endDate={t.endDate}
|
||||||
|
rightSlot={
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
hasReview
|
||||||
|
? "text-secondary-700"
|
||||||
|
: "font-bold text-amber-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{hasReview ? "Ubah ulasan →" : "Beri ulasan →"}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-8 lg:grid-cols-2">
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
|
||||||
|
Trip yang kamu buat
|
||||||
|
<span className="ml-2 text-sm font-normal text-neutral-400">
|
||||||
|
({organizedTrips.length})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
{organizedTrips.length === 0 ? (
|
||||||
|
<p className="rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-6 text-center text-sm text-neutral-500">
|
||||||
|
Belum ada trip.{" "}
|
||||||
|
<Link href="/create-trip" className="font-semibold text-primary-600">
|
||||||
|
Buat trip pertama
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{organizedTrips.map((trip) => (
|
||||||
|
<TripCard
|
||||||
|
key={trip.id}
|
||||||
|
id={trip.id}
|
||||||
|
title={trip.title}
|
||||||
|
mountain={trip.mountain}
|
||||||
|
location={trip.location}
|
||||||
|
date={trip.date}
|
||||||
|
endDate={trip.endDate}
|
||||||
|
price={trip.price}
|
||||||
|
maxParticipants={trip.maxParticipants}
|
||||||
|
participantCount={trip._count.participants}
|
||||||
|
organizerName={`${user.name} (Kamu)`}
|
||||||
|
status={trip.status}
|
||||||
|
coverImage={trip.images[0]?.url}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
|
||||||
|
Trip yang kamu ikuti
|
||||||
|
<span className="ml-2 text-sm font-normal text-neutral-400">
|
||||||
|
({activeJoined.length})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
{activeJoined.length === 0 ? (
|
||||||
|
<p className="rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-6 text-center text-sm text-neutral-500">
|
||||||
|
Belum join trip.{" "}
|
||||||
|
<Link href="/trips" className="font-semibold text-primary-600">
|
||||||
|
Cari open trip
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{activeJoined.map((p) => {
|
||||||
|
const t = p.trip;
|
||||||
|
return (
|
||||||
|
<li key={p.id}>
|
||||||
|
<ProfileTripRow
|
||||||
|
href={`/trips/${t.id}`}
|
||||||
|
title={t.title}
|
||||||
|
mountain={t.mountain}
|
||||||
|
date={t.date}
|
||||||
|
endDate={t.endDate}
|
||||||
|
rightSlot={
|
||||||
|
<span className="text-neutral-400">
|
||||||
|
{p.status === "CONFIRMED" ? "Terdaftar" : p.status}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cancelledJoined.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-neutral-500">
|
||||||
|
Riwayat batal ({cancelledJoined.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2 opacity-80">
|
||||||
|
{cancelledJoined.map((p) => {
|
||||||
|
const t = p.trip;
|
||||||
|
return (
|
||||||
|
<li key={p.id}>
|
||||||
|
<ProfileTripRow
|
||||||
|
href={`/trips/${t.id}`}
|
||||||
|
title={t.title}
|
||||||
|
mountain={t.mountain}
|
||||||
|
date={t.date}
|
||||||
|
endDate={t.endDate}
|
||||||
|
rightSlot={
|
||||||
|
<span className="text-red-500/90">Dibatalkan</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,11 @@ import { tripService } from "@/server/services/trip.service";
|
|||||||
import { formatRupiah, formatDateRange } from "@/lib/utils";
|
import { formatRupiah, formatDateRange } from "@/lib/utils";
|
||||||
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
||||||
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
||||||
|
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||||
|
import {
|
||||||
|
isPastTripLastDayForReview,
|
||||||
|
isTripDepartureDayPast,
|
||||||
|
} from "@/lib/trip-dates";
|
||||||
|
|
||||||
export default async function TripDetailPage({
|
export default async function TripDetailPage({
|
||||||
params,
|
params,
|
||||||
@@ -38,6 +43,26 @@ export default async function TripDetailPage({
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const isDeparturePast = isTripDepartureDayPast(trip.date);
|
||||||
|
const canReview =
|
||||||
|
!!session?.user &&
|
||||||
|
!isOrganizer &&
|
||||||
|
currentParticipation?.status === "CONFIRMED" &&
|
||||||
|
isPastTripLastDayForReview(trip.date, trip.endDate);
|
||||||
|
|
||||||
|
const myReview = session?.user
|
||||||
|
? trip.reviews.find((r) => r.userId === session.user.id) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const averageRating =
|
||||||
|
trip.reviews.length > 0
|
||||||
|
? Math.round(
|
||||||
|
(trip.reviews.reduce((s, r) => s + r.rating, 0) /
|
||||||
|
trip.reviews.length) *
|
||||||
|
10
|
||||||
|
) / 10
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
|
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
@@ -180,6 +205,25 @@ export default async function TripDetailPage({
|
|||||||
isJoined={!!currentParticipation}
|
isJoined={!!currentParticipation}
|
||||||
isFull={spotsLeft <= 0}
|
isFull={spotsLeft <= 0}
|
||||||
tripStatus={trip.status}
|
tripStatus={trip.status}
|
||||||
|
isDeparturePast={isDeparturePast}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TripReviewSection
|
||||||
|
tripId={trip.id}
|
||||||
|
reviews={trip.reviews.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
rating: r.rating,
|
||||||
|
comment: r.comment,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
user: r.user,
|
||||||
|
}))}
|
||||||
|
averageRating={averageRating}
|
||||||
|
canReview={canReview}
|
||||||
|
myReview={
|
||||||
|
myReview
|
||||||
|
? { rating: myReview.rating, comment: myReview.comment }
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Participants List */}
|
{/* Participants List */}
|
||||||
|
|||||||
@@ -44,13 +44,26 @@ export function Navbar() {
|
|||||||
>
|
>
|
||||||
Buat Trip
|
Buat Trip
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Profil
|
||||||
|
</Link>
|
||||||
<div className="ml-2 flex items-center gap-2">
|
<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">
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-600 text-xs font-bold text-white hover:ring-2 hover:ring-primary-300"
|
||||||
|
title="Profil"
|
||||||
|
>
|
||||||
{session.user.name?.charAt(0).toUpperCase()}
|
{session.user.name?.charAt(0).toUpperCase()}
|
||||||
</div>
|
</Link>
|
||||||
<span className="text-sm font-medium text-neutral-700">
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="max-w-[140px] truncate text-sm font-medium text-neutral-700 hover:text-primary-600"
|
||||||
|
>
|
||||||
{session.user.name}
|
{session.user.name}
|
||||||
</span>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut()}
|
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"
|
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"
|
||||||
@@ -116,15 +129,26 @@ export function Navbar() {
|
|||||||
>
|
>
|
||||||
Buat Trip
|
Buat Trip
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="rounded-lg px-3 py-2.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50"
|
||||||
|
>
|
||||||
|
Profil
|
||||||
|
</Link>
|
||||||
<div className="mt-2 flex items-center justify-between rounded-lg bg-neutral-50 px-3 py-2.5">
|
<div className="mt-2 flex items-center justify-between rounded-lg bg-neutral-50 px-3 py-2.5">
|
||||||
<div className="flex items-center gap-2">
|
<Link
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-600 text-xs font-bold text-white">
|
href="/profile"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary-600 text-xs font-bold text-white">
|
||||||
{session.user.name?.charAt(0).toUpperCase()}
|
{session.user.name?.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-700">
|
<span className="truncate text-sm font-medium text-neutral-700">
|
||||||
{session.user.name}
|
{session.user.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="rounded-lg px-3 py-1.5 text-sm font-medium text-red-500 transition-colors hover:bg-red-50"
|
className="rounded-lg px-3 py-1.5 text-sm font-medium text-red-500 transition-colors hover:bg-red-50"
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = z.object({
|
||||||
email: z.email("Email tidak valid"),
|
email: z
|
||||||
password: z.string().min(6, "Password minimal 6 karakter"),
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Email harus diisi")
|
||||||
|
.pipe(z.email("Email tidak valid")),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, "Password minimal 6 karakter")
|
||||||
|
.max(LIMITS.MAX_PASSWORD_LENGTH, "Password terlalu panjang"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerSchema = z.object({
|
export const registerSchema = z.object({
|
||||||
name: z.string().min(2, "Nama minimal 2 karakter"),
|
name: z
|
||||||
email: z.email("Email tidak valid"),
|
.string()
|
||||||
password: z.string().min(6, "Password minimal 6 karakter"),
|
.trim()
|
||||||
|
.min(2, "Nama minimal 2 karakter")
|
||||||
|
.max(LIMITS.MAX_NAME_LENGTH, `Nama maksimal ${LIMITS.MAX_NAME_LENGTH} karakter`),
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Email harus diisi")
|
||||||
|
.pipe(z.email("Email tidak valid")),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, "Password minimal 6 karakter")
|
||||||
|
.max(LIMITS.MAX_PASSWORD_LENGTH, "Password terlalu panjang (maks. 72 karakter)"),
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
}).refine((data) => data.password === data.confirmPassword, {
|
}).refine((data) => data.password === data.confirmPassword, {
|
||||||
message: "Password tidak cocok",
|
message: "Password tidak cocok",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { formatDateRange } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface ProfileTripRowProps {
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
mountain: string;
|
||||||
|
date: Date;
|
||||||
|
endDate: Date | null;
|
||||||
|
rightSlot?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileTripRow({
|
||||||
|
href,
|
||||||
|
title,
|
||||||
|
mountain,
|
||||||
|
date,
|
||||||
|
endDate,
|
||||||
|
rightSlot,
|
||||||
|
}: ProfileTripRowProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-xl border border-neutral-200 bg-white px-3 py-2.5 transition-colors hover:border-primary-200 hover:bg-primary-50/40 sm:px-4 sm:py-3"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-semibold text-neutral-800">{title}</p>
|
||||||
|
<p className="truncate text-xs text-neutral-500">{mountain}</p>
|
||||||
|
<p className="mt-0.5 text-[11px] text-neutral-400 sm:text-xs">
|
||||||
|
{formatDateRange(date, endDate)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{rightSlot && (
|
||||||
|
<div className="shrink-0 text-right text-xs font-medium">{rightSlot}</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { upsertReviewSchema } from "./schemas";
|
||||||
|
import { reviewService } from "@/server/services/review.service";
|
||||||
|
|
||||||
|
export async function upsertTripReviewAction(formData: FormData) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) {
|
||||||
|
return { error: "Kamu harus login terlebih dahulu" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = {
|
||||||
|
tripId: formData.get("tripId") as string,
|
||||||
|
rating: formData.get("rating") as string,
|
||||||
|
comment: String(formData.get("comment") ?? ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsed = upsertReviewSchema.safeParse(raw);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return { error: parsed.error.issues[0]?.message ?? "Data tidak valid" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await reviewService.upsertReview(
|
||||||
|
parsed.data.tripId,
|
||||||
|
session.user.id,
|
||||||
|
{
|
||||||
|
rating: parsed.data.rating,
|
||||||
|
comment: parsed.data.comment,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
revalidatePath(`/trips/${parsed.data.tripId}`);
|
||||||
|
revalidatePath("/profile");
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { upsertTripReviewAction } from "@/features/review/actions";
|
||||||
|
|
||||||
|
type ReviewUser = { id: string; name: string; image: string | null };
|
||||||
|
|
||||||
|
export type TripReviewItem = {
|
||||||
|
id: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
user: ReviewUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TripReviewSectionProps {
|
||||||
|
tripId: string;
|
||||||
|
reviews: TripReviewItem[];
|
||||||
|
averageRating: number | null;
|
||||||
|
canReview: boolean;
|
||||||
|
myReview: { rating: number; comment: string | null } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TripReviewSection({
|
||||||
|
tripId,
|
||||||
|
reviews,
|
||||||
|
averageRating,
|
||||||
|
canReview,
|
||||||
|
myReview,
|
||||||
|
}: TripReviewSectionProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [rating, setRating] = useState(myReview?.rating ?? 5);
|
||||||
|
const [comment, setComment] = useState(myReview?.comment ?? "");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.set("tripId", tripId);
|
||||||
|
fd.set("rating", String(rating));
|
||||||
|
fd.set("comment", comment);
|
||||||
|
const result = await upsertTripReviewAction(fd);
|
||||||
|
setLoading(false);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
} else {
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-neutral-200 bg-white p-4 sm:p-5">
|
||||||
|
<div className="mb-4 flex flex-wrap items-baseline justify-between gap-2">
|
||||||
|
<h2 className="text-sm font-bold text-neutral-800 sm:text-base">
|
||||||
|
Ulasan peserta
|
||||||
|
</h2>
|
||||||
|
{averageRating != null && reviews.length > 0 && (
|
||||||
|
<p className="text-sm text-neutral-600">
|
||||||
|
<span className="font-bold text-amber-600">{averageRating}</span>
|
||||||
|
<span className="text-neutral-400"> /5</span>
|
||||||
|
<span className="text-neutral-400"> · {reviews.length} ulasan</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reviews.length === 0 && !canReview && (
|
||||||
|
<p className="text-xs text-neutral-500 sm:text-sm">
|
||||||
|
Belum ada ulasan. Peserta bisa menulis ulasan setelah trip selesai.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reviews.length > 0 && (
|
||||||
|
<ul className="mb-5 space-y-3 border-b border-neutral-100 pb-5">
|
||||||
|
{reviews.map((r) => (
|
||||||
|
<li
|
||||||
|
key={r.id}
|
||||||
|
className="rounded-lg bg-neutral-50 px-3 py-2.5 sm:px-4 sm:py-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-semibold text-neutral-800 sm:text-sm">
|
||||||
|
{r.user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-amber-600">
|
||||||
|
{r.rating}/5
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{r.comment && (
|
||||||
|
<p className="mt-1.5 text-xs leading-relaxed text-neutral-600 sm:text-sm">
|
||||||
|
{r.comment}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canReview && (
|
||||||
|
<div>
|
||||||
|
<p className="mb-3 text-xs text-neutral-500 sm:text-sm">
|
||||||
|
{myReview
|
||||||
|
? "Ubah ulasan kamu untuk trip ini."
|
||||||
|
: "Bagikan pengalamanmu setelah trip selesai."}
|
||||||
|
</p>
|
||||||
|
{error && (
|
||||||
|
<div className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-xs font-medium text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-semibold text-neutral-700 sm:text-sm">
|
||||||
|
Rating
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{[1, 2, 3, 4, 5].map((n) => (
|
||||||
|
<button
|
||||||
|
key={n}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRating(n)}
|
||||||
|
className={`rounded-lg px-2.5 py-1.5 text-xs font-bold sm:px-3 sm:text-sm ${
|
||||||
|
rating === n
|
||||||
|
? "bg-primary-600 text-white"
|
||||||
|
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="review-comment"
|
||||||
|
className="mb-1.5 block text-xs font-semibold text-neutral-700 sm:text-sm"
|
||||||
|
>
|
||||||
|
Komentar (opsional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="review-comment"
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
maxLength={500}
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-xs text-neutral-800 placeholder:text-neutral-400 focus:bg-white sm:text-sm"
|
||||||
|
placeholder="Ceritakan pengalaman trip..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-xl bg-secondary-600 py-2.5 text-xs font-bold text-white shadow-md hover:bg-secondary-700 disabled:opacity-50 sm:text-sm"
|
||||||
|
>
|
||||||
|
{loading ? "Menyimpan..." : myReview ? "Perbarui ulasan" : "Kirim ulasan"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
|
||||||
|
export const upsertReviewSchema = z.object({
|
||||||
|
tripId: z.string().min(1, "Trip tidak valid"),
|
||||||
|
rating: z.coerce
|
||||||
|
.number()
|
||||||
|
.int("Rating harus bilangan bulat")
|
||||||
|
.min(1, "Rating minimal 1")
|
||||||
|
.max(5, "Rating maksimal 5"),
|
||||||
|
comment: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_REVIEW_COMMENT,
|
||||||
|
`Komentar maksimal ${LIMITS.MAX_REVIEW_COMMENT} karakter`
|
||||||
|
)
|
||||||
|
.transform((s) => {
|
||||||
|
const t = s.trim();
|
||||||
|
return t === "" ? undefined : t;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpsertReviewInput = z.infer<typeof upsertReviewSchema>;
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { createTripSchema } from "./schemas";
|
import { createTripSchema, tripImageUrlsSchema } from "./schemas";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { tripStoredInstantFromYmd } from "@/lib/trip-dates";
|
||||||
|
|
||||||
export async function createTripAction(formData: FormData) {
|
export async function createTripAction(formData: FormData) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
@@ -28,21 +29,37 @@ export async function createTripAction(formData: FormData) {
|
|||||||
return { error: result.error.issues[0].message };
|
return { error: result.error.issues[0].message };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect image URLs from form (multiple inputs named "imageUrls")
|
const imageUrlsRaw = formData
|
||||||
const imageUrls = formData
|
|
||||||
.getAll("imageUrls")
|
.getAll("imageUrls")
|
||||||
.map((v) => (v as string).trim())
|
.map((v) => (v as string).trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const imagesParsed = tripImageUrlsSchema.safeParse(imageUrlsRaw);
|
||||||
|
if (!imagesParsed.success) {
|
||||||
|
return { error: imagesParsed.error.issues[0].message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrls = imagesParsed.data;
|
||||||
|
|
||||||
|
const date = tripStoredInstantFromYmd(result.data.date);
|
||||||
|
let endDate = result.data.endDate
|
||||||
|
? tripStoredInstantFromYmd(result.data.endDate)
|
||||||
|
: undefined;
|
||||||
|
if (endDate && endDate.getTime() === date.getTime()) {
|
||||||
|
endDate = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const trip = await tripService.createTrip({
|
const trip = await tripService.createTrip({
|
||||||
...result.data,
|
...result.data,
|
||||||
date: new Date(result.data.date),
|
date,
|
||||||
endDate: result.data.endDate ? new Date(result.data.endDate) : undefined,
|
endDate,
|
||||||
organizerId: session.user.id,
|
organizerId: session.user.id,
|
||||||
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
||||||
});
|
});
|
||||||
revalidatePath("/trips");
|
revalidatePath("/trips");
|
||||||
|
revalidatePath("/");
|
||||||
|
revalidatePath("/profile");
|
||||||
return { success: true, tripId: trip.id };
|
return { success: true, tripId: trip.id };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: (err as Error).message };
|
return { error: (err as Error).message };
|
||||||
@@ -58,6 +75,9 @@ export async function joinTripAction(tripId: string) {
|
|||||||
try {
|
try {
|
||||||
await tripService.joinTrip(tripId, session.user.id);
|
await tripService.joinTrip(tripId, session.user.id);
|
||||||
revalidatePath(`/trips/${tripId}`);
|
revalidatePath(`/trips/${tripId}`);
|
||||||
|
revalidatePath("/trips");
|
||||||
|
revalidatePath("/");
|
||||||
|
revalidatePath("/profile");
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: (err as Error).message };
|
return { error: (err as Error).message };
|
||||||
@@ -73,6 +93,9 @@ export async function cancelJoinAction(tripId: string) {
|
|||||||
try {
|
try {
|
||||||
await tripService.cancelJoin(tripId, session.user.id);
|
await tripService.cancelJoin(tripId, session.user.id);
|
||||||
revalidatePath(`/trips/${tripId}`);
|
revalidatePath(`/trips/${tripId}`);
|
||||||
|
revalidatePath("/trips");
|
||||||
|
revalidatePath("/");
|
||||||
|
revalidatePath("/profile");
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: (err as Error).message };
|
return { error: (err as Error).message };
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface JoinTripButtonProps {
|
|||||||
isJoined: boolean;
|
isJoined: boolean;
|
||||||
isFull: boolean;
|
isFull: boolean;
|
||||||
tripStatus: string;
|
tripStatus: string;
|
||||||
|
/** Tanggal berangkat sudah lewat (hari kalender UTC) */
|
||||||
|
isDeparturePast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JoinTripButton({
|
export function JoinTripButton({
|
||||||
@@ -21,6 +23,7 @@ export function JoinTripButton({
|
|||||||
isJoined,
|
isJoined,
|
||||||
isFull,
|
isFull,
|
||||||
tripStatus,
|
tripStatus,
|
||||||
|
isDeparturePast,
|
||||||
}: JoinTripButtonProps) {
|
}: JoinTripButtonProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -45,6 +48,24 @@ export function JoinTripButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isJoined && isDeparturePast) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl bg-neutral-100 px-3 py-3 text-center text-sm font-medium leading-relaxed text-neutral-600">
|
||||||
|
Kamu terdaftar di trip ini. Setelah tanggal berangkat lewat,{" "}
|
||||||
|
<span className="font-semibold text-neutral-700">pembatalan ditutup</span>
|
||||||
|
. Jika trip sudah selesai, isi ulasan di bagian bawah halaman.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeparturePast && !isJoined) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl bg-neutral-100 py-3 text-center text-sm font-medium text-neutral-500">
|
||||||
|
Trip sudah lewat tanggal berangkat
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (tripStatus !== "OPEN" && !isJoined) {
|
if (tripStatus !== "OPEN" && !isJoined) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-neutral-100 py-3 text-center text-sm font-medium text-neutral-500">
|
<div className="rounded-xl bg-neutral-100 py-3 text-center text-sm font-medium text-neutral-500">
|
||||||
|
|||||||
@@ -90,8 +90,12 @@ export function TripFilter() {
|
|||||||
{/* Date range */}
|
{/* Date range */}
|
||||||
<div className="sm:w-64">
|
<div className="sm:w-64">
|
||||||
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
||||||
Rentang tanggal berangkat
|
Rentang tanggal (UTC)
|
||||||
</label>
|
</label>
|
||||||
|
<p className="mb-1 text-[10px] leading-snug text-neutral-400 sm:text-xs">
|
||||||
|
Menampilkan trip yang jadwalnya overlap rentang ini: multi hari pakai
|
||||||
|
tanggal pulang; satu hari pakai tanggal berangkat saja.
|
||||||
|
</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 z-10 -translate-y-1/2 text-neutral-400">
|
<span className="absolute left-3 top-1/2 z-10 -translate-y-1/2 text-neutral-400">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
+106
-13
@@ -1,17 +1,110 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
import {
|
||||||
|
isTripDepartureDayPast,
|
||||||
|
tripStoredInstantFromYmd,
|
||||||
|
} from "@/lib/trip-dates";
|
||||||
|
|
||||||
export const createTripSchema = z.object({
|
export const tripImageUrlsSchema = z
|
||||||
title: z.string().min(3, "Judul minimal 3 karakter"),
|
.array(
|
||||||
description: z.string().optional(),
|
z
|
||||||
mountain: z.string().min(2, "Nama gunung harus diisi"),
|
.string()
|
||||||
location: z.string().min(2, "Lokasi harus diisi"),
|
.trim()
|
||||||
date: z.string().refine((val) => !isNaN(Date.parse(val)), "Tanggal berangkat tidak valid"),
|
.max(LIMITS.MAX_URL_LENGTH, "URL terlalu panjang")
|
||||||
endDate: z
|
.url("Setiap URL gambar harus valid (http/https)")
|
||||||
.string()
|
)
|
||||||
.optional()
|
.max(LIMITS.MAX_IMAGE_URLS, `Maksimal ${LIMITS.MAX_IMAGE_URLS} foto`);
|
||||||
.refine((val) => !val || !isNaN(Date.parse(val)), "Tanggal pulang tidak valid"),
|
|
||||||
maxParticipants: z.coerce.number().min(1, "Minimal 1 peserta"),
|
export const createTripSchema = z
|
||||||
price: z.coerce.number().min(0, "Harga tidak valid"),
|
.object({
|
||||||
});
|
title: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(3, "Judul minimal 3 karakter")
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_TITLE_LENGTH,
|
||||||
|
`Judul maksimal ${LIMITS.MAX_TITLE_LENGTH} karakter`
|
||||||
|
),
|
||||||
|
description: z.preprocess(
|
||||||
|
(val) => {
|
||||||
|
if (val == null) return undefined;
|
||||||
|
const s = String(val).trim();
|
||||||
|
return s === "" ? undefined : s;
|
||||||
|
},
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_DESCRIPTION_LENGTH,
|
||||||
|
`Deskripsi maksimal ${LIMITS.MAX_DESCRIPTION_LENGTH} karakter`
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
),
|
||||||
|
mountain: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(2, "Nama gunung harus diisi")
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_MOUNTAIN_LENGTH,
|
||||||
|
`Nama gunung maksimal ${LIMITS.MAX_MOUNTAIN_LENGTH} karakter`
|
||||||
|
),
|
||||||
|
location: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(2, "Lokasi harus diisi")
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_LOCATION_LENGTH,
|
||||||
|
`Lokasi maksimal ${LIMITS.MAX_LOCATION_LENGTH} karakter`
|
||||||
|
),
|
||||||
|
date: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => !Number.isNaN(Date.parse(val)), "Tanggal berangkat tidak valid"),
|
||||||
|
endDate: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine(
|
||||||
|
(val) => !val || !Number.isNaN(Date.parse(val)),
|
||||||
|
"Tanggal pulang tidak valid"
|
||||||
|
),
|
||||||
|
maxParticipants: z.coerce
|
||||||
|
.number()
|
||||||
|
.int("Jumlah peserta harus bilangan bulat")
|
||||||
|
.min(
|
||||||
|
LIMITS.MIN_PARTICIPANTS,
|
||||||
|
`Minimal ${LIMITS.MIN_PARTICIPANTS} peserta`
|
||||||
|
)
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_PARTICIPANTS,
|
||||||
|
`Maksimal ${LIMITS.MAX_PARTICIPANTS} peserta`
|
||||||
|
),
|
||||||
|
price: z.coerce
|
||||||
|
.number()
|
||||||
|
.int("Harga harus bilangan bulat (tanpa desimal)")
|
||||||
|
.min(0, "Harga tidak valid")
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_PRICE_IDR,
|
||||||
|
`Harga maksimal Rp ${LIMITS.MAX_PRICE_IDR.toLocaleString("id-ID")}`
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const dep = tripStoredInstantFromYmd(data.date);
|
||||||
|
if (!Number.isNaN(dep.getTime()) && isTripDepartureDayPast(dep)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Tanggal berangkat tidak boleh di masa lalu",
|
||||||
|
path: ["date"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.endDate) {
|
||||||
|
const startY = data.date.slice(0, 10);
|
||||||
|
const endY = data.endDate.slice(0, 10);
|
||||||
|
if (endY < startY) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Tanggal pulang tidak boleh sebelum tanggal berangkat",
|
||||||
|
path: ["endDate"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export type CreateTripInput = z.infer<typeof createTripSchema>;
|
export type CreateTripInput = z.infer<typeof createTripSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/** Batasan bisnis & input (Phase 2) */
|
||||||
|
export const LIMITS = {
|
||||||
|
/** Maks trip yang bisa dibuat satu organizer per hari kalender (UTC) */
|
||||||
|
MAX_TRIPS_PER_ORGANIZER_PER_DAY: 15,
|
||||||
|
MIN_PARTICIPANTS: 1,
|
||||||
|
MAX_PARTICIPANTS: 99,
|
||||||
|
MAX_PRICE_IDR: 100_000_000,
|
||||||
|
MAX_TITLE_LENGTH: 120,
|
||||||
|
MAX_MOUNTAIN_LENGTH: 100,
|
||||||
|
MAX_LOCATION_LENGTH: 120,
|
||||||
|
MAX_DESCRIPTION_LENGTH: 5000,
|
||||||
|
MAX_REVIEW_COMMENT: 500,
|
||||||
|
MAX_IMAGE_URLS: 5,
|
||||||
|
MAX_URL_LENGTH: 2048,
|
||||||
|
MAX_NAME_LENGTH: 80,
|
||||||
|
MAX_PASSWORD_LENGTH: 72,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/** Bantu perbandingan tanggal trip (hari kalender UTC). */
|
||||||
|
|
||||||
|
/** Parse `YYYY-MM-DD` sebagai awal hari UTC (hindari ambiguitas `new Date(string)` di berbagai TZ). */
|
||||||
|
export function utcDayStartFromYmd(ymd: string): Date {
|
||||||
|
const parts = ymd.split("-").map(Number);
|
||||||
|
const y = parts[0];
|
||||||
|
const m = parts[1];
|
||||||
|
const d = parts[2];
|
||||||
|
if (!y || !m || !d) {
|
||||||
|
return utcStartOfDay(new Date());
|
||||||
|
}
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Akhir hari UTC untuk `YYYY-MM-DD`. */
|
||||||
|
export function utcDayEndFromYmd(ymd: string): Date {
|
||||||
|
const parts = ymd.split("-").map(Number);
|
||||||
|
const y = parts[0];
|
||||||
|
const m = parts[1];
|
||||||
|
const d = parts[2];
|
||||||
|
if (!y || !m || !d) {
|
||||||
|
return utcStartOfDay(new Date());
|
||||||
|
}
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, 23, 59, 59, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maxUtcDate(a: Date, b: Date): Date {
|
||||||
|
return a.getTime() >= b.getTime() ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tanggal dari DatePicker = hari kalender **lokal** browser.
|
||||||
|
* Encode `YYYY-MM-DD` dengan `getFullYear/getMonth/getDate` (bukan `toISOString()`)
|
||||||
|
* supaya tidak bergeser ke hari UTC lain (mis. WIB malam → UTC hari sebelumnya).
|
||||||
|
*/
|
||||||
|
export function formatLocalCalendarYmd(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simpan `trip.date` / `trip.endDate`: string `YYYY-MM-DD` diartikan sebagai
|
||||||
|
* **hari kalender UTC** yang sama (selaras dengan filter Open Trip).
|
||||||
|
*/
|
||||||
|
export function tripStoredInstantFromYmd(ymd: string): Date {
|
||||||
|
return utcDayStartFromYmd(ymd.trim().slice(0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function utcStartOfDay(d: Date): Date {
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true jika hari berangkat trip sudah lewat dibanding hari ini (UTC). */
|
||||||
|
export function isTripDepartureDayPast(departureDate: Date): boolean {
|
||||||
|
const today = utcStartOfDay(new Date());
|
||||||
|
const dep = utcStartOfDay(departureDate);
|
||||||
|
return dep.getTime() < today.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* true jika sudah lewat akhir hari terakhir trip (tanggal pulang atau tanggal berangkat).
|
||||||
|
* Dipakai untuk membuka fitur ulasan.
|
||||||
|
*/
|
||||||
|
export function isPastTripLastDayForReview(
|
||||||
|
tripDate: Date,
|
||||||
|
tripEndDate: Date | null
|
||||||
|
): boolean {
|
||||||
|
const last = tripEndDate ?? tripDate;
|
||||||
|
const endMs = Date.UTC(
|
||||||
|
last.getUTCFullYear(),
|
||||||
|
last.getUTCMonth(),
|
||||||
|
last.getUTCDate(),
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
999
|
||||||
|
);
|
||||||
|
return Date.now() > endMs;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TripReview" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"rating" INTEGER NOT NULL,
|
||||||
|
"comment" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"tripId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TripReview_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TripReview_tripId_userId_key" ON "TripReview"("tripId", "userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TripReview" ADD CONSTRAINT "TripReview_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "Trip"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TripReview" ADD CONSTRAINT "TripReview_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TripReview" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||||
@@ -18,6 +18,7 @@ model User {
|
|||||||
|
|
||||||
trips Trip[]
|
trips Trip[]
|
||||||
participations TripParticipant[]
|
participations TripParticipant[]
|
||||||
|
tripReviews TripReview[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Trip {
|
model Trip {
|
||||||
@@ -39,6 +40,23 @@ model Trip {
|
|||||||
|
|
||||||
participants TripParticipant[]
|
participants TripParticipant[]
|
||||||
images TripImage[]
|
images TripImage[]
|
||||||
|
reviews TripReview[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model TripReview {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
rating Int
|
||||||
|
comment String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
tripId String
|
||||||
|
trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([tripId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TripImage {
|
model TripImage {
|
||||||
|
|||||||
+33
-16
@@ -12,6 +12,7 @@ async function main() {
|
|||||||
console.log("🌱 Seeding database...\n");
|
console.log("🌱 Seeding database...\n");
|
||||||
|
|
||||||
// Clean existing data (order matters for FK)
|
// Clean existing data (order matters for FK)
|
||||||
|
await prisma.tripReview.deleteMany();
|
||||||
await prisma.tripParticipant.deleteMany();
|
await prisma.tripParticipant.deleteMany();
|
||||||
await prisma.tripImage.deleteMany();
|
await prisma.tripImage.deleteMany();
|
||||||
await prisma.trip.deleteMany();
|
await prisma.trip.deleteMany();
|
||||||
@@ -93,11 +94,18 @@ async function main() {
|
|||||||
console.log(" Password semua: password123\n");
|
console.log(" Password semua: password123\n");
|
||||||
|
|
||||||
// ==================== TRIPS + IMAGES ====================
|
// ==================== TRIPS + IMAGES ====================
|
||||||
|
/**
|
||||||
|
* Tanggal disimpan eksplisit di UTC agar filter `from`/`to` (YYYY-MM-DD UTC)
|
||||||
|
* cocok dengan yang tampil di daftar.
|
||||||
|
*
|
||||||
|
* - Multi hari: isi `endDate` = hari terakhir trip (UTC).
|
||||||
|
* - Satu hari / night hike satu malam: `endDate` null — filter memakai instan `date`
|
||||||
|
* dalam rentang hari UTC yang sama (jam tetap masuk hari itu).
|
||||||
|
*/
|
||||||
|
const utc = (y: number, m0: number, d: number, h = 12, min = 0) =>
|
||||||
|
new Date(Date.UTC(y, m0, d, h, min, 0, 0));
|
||||||
|
|
||||||
const now = new Date();
|
// --- Trip 1: Papandayan (by Dede Inoen) — 2 hari ---
|
||||||
const day = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// --- Trip 1: Papandayan (by Dede Inoen) ---
|
|
||||||
const trip1 = await prisma.trip.create({
|
const trip1 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Open Trip Papandayan Weekend",
|
title: "Open Trip Papandayan Weekend",
|
||||||
@@ -112,7 +120,8 @@ Itinerary:
|
|||||||
- Minggu: Sunrise → Turun → Pulang`,
|
- Minggu: Sunrise → Turun → Pulang`,
|
||||||
mountain: "Gunung Papandayan",
|
mountain: "Gunung Papandayan",
|
||||||
location: "Garut, Jawa Barat",
|
location: "Garut, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 3 * day),
|
date: utc(2026, 3, 23, 8, 0),
|
||||||
|
endDate: utc(2026, 3, 24, 18, 0),
|
||||||
maxParticipants: 10,
|
maxParticipants: 10,
|
||||||
price: 250000,
|
price: 250000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -127,7 +136,7 @@ Itinerary:
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Trip 2: Ciremai (by Panji Petualang) ---
|
// --- Trip 2: Ciremai (by Panji Petualang) — 2 hari ---
|
||||||
const trip2 = await prisma.trip.create({
|
const trip2 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Pendakian Ciremai via Apuy",
|
title: "Pendakian Ciremai via Apuy",
|
||||||
@@ -142,7 +151,8 @@ Itinerary:
|
|||||||
- Hari 2: Summit attack → Turun → Pulang`,
|
- Hari 2: Summit attack → Turun → Pulang`,
|
||||||
mountain: "Gunung Ciremai",
|
mountain: "Gunung Ciremai",
|
||||||
location: "Kuningan, Jawa Barat",
|
location: "Kuningan, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 5 * day),
|
date: utc(2026, 3, 25, 4, 0),
|
||||||
|
endDate: utc(2026, 3, 26, 18, 0),
|
||||||
maxParticipants: 8,
|
maxParticipants: 8,
|
||||||
price: 350000,
|
price: 350000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -157,7 +167,7 @@ Itinerary:
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Trip 3: Gede-Pangrango (by Fiersa Besari) ---
|
// --- Trip 3: Gede-Pangrango (by Fiersa Besari) — 2 hari ---
|
||||||
const trip3 = await prisma.trip.create({
|
const trip3 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Sunrise Trip Gede-Pangrango",
|
title: "Sunrise Trip Gede-Pangrango",
|
||||||
@@ -170,7 +180,8 @@ Itinerary:
|
|||||||
Start malam, summit saat sunrise. View epic dijamin!`,
|
Start malam, summit saat sunrise. View epic dijamin!`,
|
||||||
mountain: "Gunung Gede",
|
mountain: "Gunung Gede",
|
||||||
location: "Bogor/Cianjur, Jawa Barat",
|
location: "Bogor/Cianjur, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 6 * day),
|
date: utc(2026, 3, 27, 22, 0),
|
||||||
|
endDate: utc(2026, 3, 28, 16, 0),
|
||||||
maxParticipants: 12,
|
maxParticipants: 12,
|
||||||
price: 280000,
|
price: 280000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -186,7 +197,7 @@ Start malam, summit saat sunrise. View epic dijamin!`,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Trip 4: Tangkuban Parahu (by Dede Inoen) ---
|
// --- Trip 4: Tangkuban Parahu (by Dede Inoen) — 1 hari ---
|
||||||
const trip4 = await prisma.trip.create({
|
const trip4 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Trip Hemat Tangkuban Parahu",
|
title: "Trip Hemat Tangkuban Parahu",
|
||||||
@@ -199,7 +210,8 @@ Start malam, summit saat sunrise. View epic dijamin!`,
|
|||||||
Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
|
Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
|
||||||
mountain: "Gunung Tangkuban Parahu",
|
mountain: "Gunung Tangkuban Parahu",
|
||||||
location: "Bandung, Jawa Barat",
|
location: "Bandung, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 2 * day),
|
date: utc(2026, 3, 22, 0, 0),
|
||||||
|
endDate: null,
|
||||||
maxParticipants: 15,
|
maxParticipants: 15,
|
||||||
price: 120000,
|
price: 120000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -213,7 +225,7 @@ Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Trip 5: Malabar (by Fiersa Besari) ---
|
// --- Trip 5: Malabar (by Fiersa Besari) — 1 hari (night hike, `endDate` null) ---
|
||||||
const trip5 = await prisma.trip.create({
|
const trip5 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Malabar Night Hike",
|
title: "Malabar Night Hike",
|
||||||
@@ -226,7 +238,8 @@ Explore Kawah Ratu, Kawah Domas, foto-foto, terus makan sate maranggi!`,
|
|||||||
Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
|
Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
|
||||||
mountain: "Gunung Malabar",
|
mountain: "Gunung Malabar",
|
||||||
location: "Bandung, Jawa Barat",
|
location: "Bandung, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 4 * day),
|
date: utc(2026, 3, 20, 14, 0),
|
||||||
|
endDate: null,
|
||||||
maxParticipants: 10,
|
maxParticipants: 10,
|
||||||
price: 150000,
|
price: 150000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -241,7 +254,7 @@ Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Trip 6: Guntur (by Panji Petualang) ---
|
// --- Trip 6: Guntur (by Panji Petualang) — 2 hari ---
|
||||||
const trip6 = await prisma.trip.create({
|
const trip6 = await prisma.trip.create({
|
||||||
data: {
|
data: {
|
||||||
title: "Guntur Challenge Trip",
|
title: "Guntur Challenge Trip",
|
||||||
@@ -254,7 +267,8 @@ Trip ringan, 3-4 jam naik. Cocok buat yang mau healing malam-malam.`,
|
|||||||
Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`,
|
Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`,
|
||||||
mountain: "Gunung Guntur",
|
mountain: "Gunung Guntur",
|
||||||
location: "Garut, Jawa Barat",
|
location: "Garut, Jawa Barat",
|
||||||
date: new Date(now.getTime() + 10 * day),
|
date: utc(2026, 3, 30, 4, 0),
|
||||||
|
endDate: utc(2026, 4, 1, 18, 0),
|
||||||
maxParticipants: 8,
|
maxParticipants: 8,
|
||||||
price: 300000,
|
price: 300000,
|
||||||
status: "OPEN",
|
status: "OPEN",
|
||||||
@@ -269,7 +283,10 @@ Buat yang suka challenge. Pemandangan kawah aktif dari dekat!`,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ 6 Trips + images created\n");
|
console.log("✅ 6 Trips + images created (tanggal UTC + endDate untuk trip multi hari)\n");
|
||||||
|
console.log(
|
||||||
|
` Guntur: ${trip6.title} ${trip6.date.toISOString().slice(0, 10)} → ${trip6.endDate?.toISOString().slice(0, 10) ?? "-"}\n`
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== PARTICIPANTS ====================
|
// ==================== PARTICIPANTS ====================
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,31 @@ export const participantRepo = {
|
|||||||
data: { status: "CANCELLED" },
|
data: { status: "CANCELLED" },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async reactivate(tripId: string, userId: string) {
|
||||||
|
return prisma.tripParticipant.update({
|
||||||
|
where: { tripId_userId: { tripId, userId } },
|
||||||
|
data: { status: "CONFIRMED" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Partisipasi user beserta trip (untuk profil & riwayat). */
|
||||||
|
async findWithTripForProfile(userId: string) {
|
||||||
|
return prisma.tripParticipant.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
trip: {
|
||||||
|
include: {
|
||||||
|
organizer: { select: { id: true, name: true } },
|
||||||
|
images: { orderBy: { order: "asc" }, take: 1 },
|
||||||
|
reviews: {
|
||||||
|
where: { userId },
|
||||||
|
select: { id: true, rating: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export const reviewRepo = {
|
||||||
|
async upsert(data: {
|
||||||
|
tripId: string;
|
||||||
|
userId: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string | null;
|
||||||
|
}) {
|
||||||
|
return prisma.tripReview.upsert({
|
||||||
|
where: {
|
||||||
|
tripId_userId: { tripId: data.tripId, userId: data.userId },
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
tripId: data.tripId,
|
||||||
|
userId: data.userId,
|
||||||
|
rating: data.rating,
|
||||||
|
comment: data.comment,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
rating: data.rating,
|
||||||
|
comment: data.comment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { Prisma } from "@/app/generated/prisma/client";
|
import { Prisma } from "@/app/generated/prisma/client";
|
||||||
|
import {
|
||||||
|
utcStartOfDay,
|
||||||
|
utcDayStartFromYmd,
|
||||||
|
utcDayEndFromYmd,
|
||||||
|
maxUtcDate,
|
||||||
|
} from "@/lib/trip-dates";
|
||||||
|
|
||||||
export const tripRepo = {
|
export const tripRepo = {
|
||||||
async findAll() {
|
async findAll() {
|
||||||
@@ -7,44 +13,76 @@ export const tripRepo = {
|
|||||||
include: {
|
include: {
|
||||||
organizer: { select: { id: true, name: true, image: true } },
|
organizer: { select: { id: true, name: true, image: true } },
|
||||||
images: { orderBy: { order: "asc" }, take: 1 },
|
images: { orderBy: { order: "asc" }, take: 1 },
|
||||||
_count: { select: { participants: true } },
|
_count: {
|
||||||
|
select: {
|
||||||
|
participants: { where: { status: { not: "CANCELLED" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: { date: "asc" },
|
orderBy: { date: "asc" },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async findOpen(filters?: { q?: string; from?: string; to?: string }) {
|
async findOpen(filters?: { q?: string; from?: string; to?: string }) {
|
||||||
const where: Prisma.TripWhereInput = {
|
const todayStart = utcStartOfDay(new Date());
|
||||||
status: "OPEN",
|
|
||||||
date: { gte: new Date() },
|
const andParts: Prisma.TripWhereInput[] = [{ status: "OPEN" }];
|
||||||
};
|
|
||||||
|
if (!filters?.from && !filters?.to) {
|
||||||
|
andParts.push({ date: { gte: todayStart } });
|
||||||
|
} else {
|
||||||
|
const userRangeStart = filters.from
|
||||||
|
? utcDayStartFromYmd(filters.from)
|
||||||
|
: todayStart;
|
||||||
|
const userRangeEnd = filters.to
|
||||||
|
? utcDayEndFromYmd(filters.to)
|
||||||
|
: utcDayEndFromYmd("2099-12-31");
|
||||||
|
|
||||||
|
const rangeStart = maxUtcDate(todayStart, userRangeStart);
|
||||||
|
const rangeEnd = userRangeEnd;
|
||||||
|
|
||||||
|
andParts.push({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ endDate: { not: null } },
|
||||||
|
{ date: { lte: rangeEnd } },
|
||||||
|
{ endDate: { gte: rangeStart } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ endDate: null },
|
||||||
|
{ date: { gte: rangeStart } },
|
||||||
|
{ date: { lte: rangeEnd } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filters?.q) {
|
if (filters?.q) {
|
||||||
where.OR = [
|
andParts.push({
|
||||||
{ title: { contains: filters.q, mode: "insensitive" } },
|
OR: [
|
||||||
{ mountain: { contains: filters.q, mode: "insensitive" } },
|
{ title: { contains: filters.q, mode: "insensitive" } },
|
||||||
{ location: { contains: filters.q, mode: "insensitive" } },
|
{ mountain: { contains: filters.q, mode: "insensitive" } },
|
||||||
];
|
{ location: { contains: filters.q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters?.from || filters?.to) {
|
const where: Prisma.TripWhereInput = { AND: andParts };
|
||||||
const dateFilter: Prisma.DateTimeFilter = { gte: new Date() };
|
|
||||||
if (filters.from) {
|
|
||||||
const fromDate = new Date(filters.from);
|
|
||||||
if (fromDate > new Date()) dateFilter.gte = fromDate;
|
|
||||||
}
|
|
||||||
if (filters.to) {
|
|
||||||
dateFilter.lte = new Date(filters.to + "T23:59:59.999Z");
|
|
||||||
}
|
|
||||||
where.date = dateFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prisma.trip.findMany({
|
return prisma.trip.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
organizer: { select: { id: true, name: true, image: true } },
|
organizer: { select: { id: true, name: true, image: true } },
|
||||||
images: { orderBy: { order: "asc" }, take: 1 },
|
images: { orderBy: { order: "asc" }, take: 1 },
|
||||||
_count: { select: { participants: true } },
|
_count: {
|
||||||
|
select: {
|
||||||
|
participants: { where: { status: { not: "CANCELLED" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: { date: "asc" },
|
orderBy: { date: "asc" },
|
||||||
});
|
});
|
||||||
@@ -59,10 +97,38 @@ export const tripRepo = {
|
|||||||
participants: {
|
participants: {
|
||||||
include: { user: { select: { id: true, name: true, image: true } } },
|
include: { user: { select: { id: true, name: true, image: true } } },
|
||||||
},
|
},
|
||||||
|
reviews: {
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, image: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async countByOrganizerSince(organizerId: string, since: Date) {
|
||||||
|
return prisma.trip.count({
|
||||||
|
where: { organizerId, createdAt: { gte: since } },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Semua trip yang dibuat user (semua status), terbaru dulu — untuk profil. */
|
||||||
|
async findByOrganizerId(organizerId: string) {
|
||||||
|
return prisma.trip.findMany({
|
||||||
|
where: { organizerId },
|
||||||
|
include: {
|
||||||
|
images: { orderBy: { order: "asc" }, take: 1 },
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
participants: { where: { status: { not: "CANCELLED" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { date: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async create(data: Prisma.TripCreateInput) {
|
async create(data: Prisma.TripCreateInput) {
|
||||||
return prisma.trip.create({ data });
|
return prisma.trip.create({ data });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ export const userRepo = {
|
|||||||
return prisma.user.findUnique({ where: { id } });
|
return prisma.user.findUnique({ where: { id } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Profil publik (tanpa password) untuk halaman akun. */
|
||||||
|
async findPublicProfileById(id: string) {
|
||||||
|
return prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
image: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async create(data: Prisma.UserCreateInput) {
|
async create(data: Prisma.UserCreateInput) {
|
||||||
return prisma.user.create({ data });
|
return prisma.user.create({ data });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { userRepo } from "@/server/repositories/user.repo";
|
||||||
|
import { tripRepo } from "@/server/repositories/trip.repo";
|
||||||
|
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||||
|
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export const profileService = {
|
||||||
|
async getProfileDashboard(userId: string) {
|
||||||
|
const user = await userRepo.findPublicProfileById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("Pengguna tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [organizedTrips, participations] = await Promise.all([
|
||||||
|
tripRepo.findByOrganizerId(userId),
|
||||||
|
participantRepo.findWithTripForProfile(userId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeJoined = participations
|
||||||
|
.filter((p) => p.status !== "CANCELLED")
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
|
||||||
|
);
|
||||||
|
const cancelledJoined = participations
|
||||||
|
.filter((p) => p.status === "CANCELLED")
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
const reviewable = activeJoined
|
||||||
|
.filter((p) => {
|
||||||
|
if (p.status !== "CONFIRMED") return false;
|
||||||
|
const t = p.trip;
|
||||||
|
if (t.organizerId === userId) return false;
|
||||||
|
return isPastTripLastDayForReview(t.date, t.endDate);
|
||||||
|
})
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
organizedTrips,
|
||||||
|
activeJoined,
|
||||||
|
cancelledJoined,
|
||||||
|
reviewable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { tripRepo } from "@/server/repositories/trip.repo";
|
||||||
|
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||||
|
import { reviewRepo } from "@/server/repositories/review.repo";
|
||||||
|
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export const reviewService = {
|
||||||
|
async upsertReview(
|
||||||
|
tripId: string,
|
||||||
|
userId: string,
|
||||||
|
input: { rating: number; comment?: string | null }
|
||||||
|
) {
|
||||||
|
const trip = await tripRepo.findById(tripId);
|
||||||
|
if (!trip) {
|
||||||
|
throw new Error("Trip tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trip.organizerId === userId) {
|
||||||
|
throw new Error("Organizer tidak bisa mengulas trip sendiri");
|
||||||
|
}
|
||||||
|
|
||||||
|
const participation = await participantRepo.findByTripAndUser(
|
||||||
|
tripId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
if (!participation || participation.status !== "CONFIRMED") {
|
||||||
|
throw new Error(
|
||||||
|
"Hanya peserta yang terdaftar (aktif) yang bisa memberi ulasan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPastTripLastDayForReview(trip.date, trip.endDate)) {
|
||||||
|
throw new Error(
|
||||||
|
"Ulasan bisa diberikan setelah tanggal selesai trip (hari terakhir pendakian)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reviewRepo.upsert({
|
||||||
|
tripId,
|
||||||
|
userId,
|
||||||
|
rating: input.rating,
|
||||||
|
comment: input.comment ?? null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { tripRepo } from "@/server/repositories/trip.repo";
|
import { tripRepo } from "@/server/repositories/trip.repo";
|
||||||
import { participantRepo } from "@/server/repositories/participant.repo";
|
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||||
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
|
||||||
|
|
||||||
interface CreateTripInput {
|
interface CreateTripInput {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,6 +34,25 @@ export const tripService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async createTrip(input: CreateTripInput) {
|
async createTrip(input: CreateTripInput) {
|
||||||
|
const since = utcStartOfDay(new Date());
|
||||||
|
const todayCount = await tripRepo.countByOrganizerSince(
|
||||||
|
input.organizerId,
|
||||||
|
since
|
||||||
|
);
|
||||||
|
if (todayCount >= LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY) {
|
||||||
|
throw new Error(
|
||||||
|
`Batas harian: maksimal ${LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY} trip per hari (UTC). Coba lagi besok.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTripDepartureDayPast(input.date)) {
|
||||||
|
throw new Error("Tanggal berangkat tidak boleh di masa lalu");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.endDate && input.endDate.getTime() < input.date.getTime()) {
|
||||||
|
throw new Error("Tanggal pulang tidak boleh sebelum tanggal berangkat");
|
||||||
|
}
|
||||||
|
|
||||||
const images = input.imageUrls?.length
|
const images = input.imageUrls?.length
|
||||||
? {
|
? {
|
||||||
create: input.imageUrls.map((url, i) => ({ url, order: i })),
|
create: input.imageUrls.map((url, i) => ({ url, order: i })),
|
||||||
@@ -62,6 +83,12 @@ export const tripService = {
|
|||||||
throw new Error("Trip tidak tersedia untuk pendaftaran");
|
throw new Error("Trip tidak tersedia untuk pendaftaran");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTripDepartureDayPast(trip.date)) {
|
||||||
|
throw new Error(
|
||||||
|
"Trip sudah melewati tanggal berangkat, tidak bisa mendaftar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (trip.organizerId === userId) {
|
if (trip.organizerId === userId) {
|
||||||
throw new Error("Organizer tidak bisa join trip sendiri");
|
throw new Error("Organizer tidak bisa join trip sendiri");
|
||||||
}
|
}
|
||||||
@@ -77,7 +104,10 @@ export const tripService = {
|
|||||||
throw new Error("Trip sudah penuh");
|
throw new Error("Trip sudah penuh");
|
||||||
}
|
}
|
||||||
|
|
||||||
const participant = await participantRepo.create(tripId, userId);
|
const participant =
|
||||||
|
existing?.status === "CANCELLED"
|
||||||
|
? await participantRepo.reactivate(tripId, userId)
|
||||||
|
: await participantRepo.create(tripId, userId);
|
||||||
|
|
||||||
const newCount = await participantRepo.countByTrip(tripId);
|
const newCount = await participantRepo.countByTrip(tripId);
|
||||||
if (newCount >= trip.maxParticipants) {
|
if (newCount >= trip.maxParticipants) {
|
||||||
@@ -88,6 +118,17 @@ export const tripService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async cancelJoin(tripId: string, userId: string) {
|
async cancelJoin(tripId: string, userId: string) {
|
||||||
|
const trip = await tripRepo.findById(tripId);
|
||||||
|
if (!trip) {
|
||||||
|
throw new Error("Trip tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTripDepartureDayPast(trip.date)) {
|
||||||
|
throw new Error(
|
||||||
|
"Tanggal berangkat trip sudah lewat — pendaftaran tidak bisa dibatalkan. Jika trip sudah selesai, gunakan bagian ulasan."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existing = await participantRepo.findByTripAndUser(tripId, userId);
|
const existing = await participantRepo.findByTripAndUser(tripId, userId);
|
||||||
if (!existing || existing.status === "CANCELLED") {
|
if (!existing || existing.status === "CANCELLED") {
|
||||||
throw new Error("Kamu tidak terdaftar di trip ini");
|
throw new Error("Kamu tidak terdaftar di trip ini");
|
||||||
@@ -95,8 +136,7 @@ export const tripService = {
|
|||||||
|
|
||||||
const result = await participantRepo.cancel(tripId, userId);
|
const result = await participantRepo.cancel(tripId, userId);
|
||||||
|
|
||||||
const trip = await tripRepo.findById(tripId);
|
if (trip.status === "FULL") {
|
||||||
if (trip && trip.status === "FULL") {
|
|
||||||
const count = await participantRepo.countByTrip(tripId);
|
const count = await participantRepo.countByTrip(tripId);
|
||||||
if (count < trip.maxParticipants) {
|
if (count < trip.maxParticipants) {
|
||||||
await tripRepo.updateStatus(tripId, "OPEN");
|
await tripRepo.updateStatus(tripId, "OPEN");
|
||||||
|
|||||||
Reference in New Issue
Block a user