add user profile, profile vibe and trip vibe and social signal
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
# Setrip — Social Repositioning Roadmap
|
||||||
|
|
||||||
|
Status implementasi reposisi dari "open trip pendakian" → "platform untuk menemukan teman aktivitas & trip bareng".
|
||||||
|
|
||||||
|
> **Prinsip pembeda:** Setrip bukan trip-marketplace. Trip adalah kendaraan untuk koneksi sosial (stranger → teman → circle). Setiap fitur dievaluasi: apakah memperkuat **find a companion**, atau hanya **book a trip**? Kalau cuma yang kedua → tolak.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase A — Quick wins (sinyal sosial dari data yang sudah ada) ✅
|
||||||
|
|
||||||
|
Selesai. `tsc --noEmit` lulus. Migration `20260508120000_add_profile_vibe` belum di-apply ke DB.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | Hapus ikon `🏔️` hardcoded di trip detail → ikon + label kategori dinamis | ✅ | [app/trips/[id]/page.tsx](app/trips/[id]/page.tsx) |
|
||||||
|
| A2 | Banner onboarding profil di layout (muncul kalau `UserProfile` kosong) | ✅ | [components/shared/profile-nudge-banner.tsx](components/shared/profile-nudge-banner.tsx), [app/layout.tsx](app/layout.tsx) |
|
||||||
|
| A3 | Confirmed-peserta dirombak: chip nama → kartu (avatar + kota + 3 tag minat) | ✅ | [server/repositories/trip.repo.ts](server/repositories/trip.repo.ts), [app/trips/[id]/page.tsx](app/trips/[id]/page.tsx) |
|
||||||
|
| A4 | Field `vibe` (CHILL/BALANCED/HARDCORE) di `UserProfile` + UI editor + badge di profil publik | ✅ | [prisma/schema.prisma](prisma/schema.prisma), [prisma/migrations/20260508120000_add_profile_vibe/migration.sql](prisma/migrations/20260508120000_add_profile_vibe/migration.sql), [lib/vibe.ts](lib/vibe.ts), [features/profile/schemas.ts](features/profile/schemas.ts), [features/profile/actions.ts](features/profile/actions.ts), [server/repositories/profile.repo.ts](server/repositories/profile.repo.ts), [server/repositories/user.repo.ts](server/repositories/user.repo.ts), [server/services/profile.service.ts](server/services/profile.service.ts), [features/profile/components/profile-editor.tsx](features/profile/components/profile-editor.tsx), [app/profile/page.tsx](app/profile/page.tsx), [app/u/[id]/page.tsx](app/u/[id]/page.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:** jalankan `npx prisma migrate deploy` (atau `dev`) untuk apply migration `20260507185257_add_user_profile` + `20260508120000_add_profile_vibe`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Discovery (people-first, bukan price-first) ✅
|
||||||
|
|
||||||
|
Selesai. `tsc --noEmit` lulus. Migration `20260508130000_add_trip_vibe` belum di-apply ke DB.
|
||||||
|
|
||||||
|
**Keputusan desain:** `Trip` reuse enum `Vibe` yang sama dengan `UserProfile` (alih-alih bikin `pace`/`level` baru) supaya matching peserta↔trip langsung selaras tanpa mapping.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| B1 | Halaman `/people` — daftar user dengan profil terisi | ✅ | [app/people/page.tsx](app/people/page.tsx), [server/repositories/user.repo.ts](server/repositories/user.repo.ts) (`findPeople`), [server/services/profile.service.ts](server/services/profile.service.ts) |
|
||||||
|
| B2 | Filter kota, interests, vibe di `/people` | ✅ | [features/profile/components/people-filter.tsx](features/profile/components/people-filter.tsx), [features/profile/components/user-card.tsx](features/profile/components/user-card.tsx) |
|
||||||
|
| B3 | Field `vibe` di `Trip` + tampil di trip detail & TripCard + filter di `/trips` | ✅ | [prisma/schema.prisma](prisma/schema.prisma), [prisma/migrations/20260508130000_add_trip_vibe/migration.sql](prisma/migrations/20260508130000_add_trip_vibe/migration.sql), [features/trip/schemas.ts](features/trip/schemas.ts), [features/trip/actions.ts](features/trip/actions.ts), [server/services/trip.service.ts](server/services/trip.service.ts), [features/trip/components/create-trip-form.tsx](features/trip/components/create-trip-form.tsx), [features/trip/components/trip-card.tsx](features/trip/components/trip-card.tsx), [app/trips/[id]/page.tsx](app/trips/[id]/page.tsx) |
|
||||||
|
| B4 | TripCard: 3 avatar peserta confirmed + counter `+N` | ✅ | [server/repositories/trip.repo.ts](server/repositories/trip.repo.ts) (include participants di `findOpen`), [features/trip/components/trip-card.tsx](features/trip/components/trip-card.tsx), [app/page.tsx](app/page.tsx), [app/trips/page.tsx](app/trips/page.tsx) |
|
||||||
|
| B5 | TripCard: badge "✨ X peserta sama minat" untuk user login | ✅ | [features/trip/components/trip-card.tsx](features/trip/components/trip-card.tsx) (compute overlap), homepage & `/trips` (fetch viewer interests) |
|
||||||
|
| B6 | Filter ukuran grup (Small ≤10 / Medium 11–20 / Large 21+) | ✅ | [server/repositories/trip.repo.ts](server/repositories/trip.repo.ts) (`GroupSize` filter), [features/trip/components/trip-filter.tsx](features/trip/components/trip-filter.tsx), [app/trips/page.tsx](app/trips/page.tsx) |
|
||||||
|
| B7 | Section "Budget Friendly" → "Lagi Ramai" (social proof) | ✅ | [app/page.tsx](app/page.tsx) — sort by `participantCount desc`, framing "kamu nggak bakal jalan sendirian" |
|
||||||
|
| B+ | Link `/people` di navbar (desktop + mobile) | ✅ | [components/shared/navbar.tsx](components/shared/navbar.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:** jalankan `npx prisma migrate deploy` untuk apply migration `20260508130000_add_trip_vibe` (selain 2 migration Phase A yang masih pending).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Interaksi & continuity (separate, lebih besar) ⏳
|
||||||
|
|
||||||
|
Belum mulai. Setiap item bisa jadi PR terpisah karena perlu schema baru + UI substansial.
|
||||||
|
|
||||||
|
| # | Item | Status | Catatan |
|
||||||
|
|---|---|---|---|
|
||||||
|
| C1 | Model `TripMessage` — Q&A publik per trip (sebelum berangkat) | ⏳ pending | Schema + actions + UI di trip detail. Calon peserta tanya organizer tanpa keluar app. |
|
||||||
|
| C2 | Group chat untuk peserta CONFIRMED (post-confirmation) | ⏳ pending | Bisa pakai tabel `TripMessage` yang sama dengan flag `audience` (PUBLIC/CONFIRMED_ONLY). |
|
||||||
|
| C3 | Model `Connection` (follow / circle) antar user | ⏳ pending | Foundation untuk "from strangers → circle". Halaman "Circle saya". |
|
||||||
|
| C4 | Notifikasi: organizer punya pending join, peserta dapat balasan Q&A, dst | ⏳ pending | Bisa email dulu, in-app belakangan. |
|
||||||
|
| C5 | Post-trip continuity: tombol "follow buddies dari trip ini" + album foto bareng | ⏳ pending | Momen konversi stranger → circle terbesar saat ini terbuang. |
|
||||||
|
| C6 | Review user (bukan cuma trip) — reputasi peserta (no-show? kooperatif?) | ⏳ pending | Lengkapi trust layer. Anti-scam. |
|
||||||
|
| C7 | Onboarding flow wajib post-register (bukan banner) — minta minimal 3 interests + city + vibe sebelum bisa join trip | ⏳ pending | Banner Phase A2 cuma soft nudge. Hard-gate saat user pertama kali pencet "Join". |
|
||||||
|
| C8 | Referral / invite-with-link | ⏳ pending | Loop pertumbuhan komunitas. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ Anti-list (yang harus DITOLAK kalau muncul)
|
||||||
|
|
||||||
|
Fitur-fitur ini akan menarik Setrip ke arena OTA (Traveloka/Klook) yang tidak bisa dimenangkan:
|
||||||
|
|
||||||
|
- Booking hotel / tiket pesawat
|
||||||
|
- Tour massal tanpa interaksi (>30 orang, bus pariwisata)
|
||||||
|
- Mass listing dari travel agent (B2B aggregator)
|
||||||
|
- Filter & sort harga yang lebih agresif (price-low-to-high, dll.) — perkuat framing harga-dulu
|
||||||
|
- Affiliate/komisi dari pihak ketiga yang bukan organizer terverifikasi
|
||||||
|
- SEO-driven mass content untuk destinasi (artikel "10 Gunung Terbaik di Jawa") tanpa angle social
|
||||||
|
- Integrasi pembayaran kompleks (split-bill, escrow rumit) sebelum chat dasar (C1) ada — prioritas terbalik
|
||||||
|
|
||||||
|
Kalau muncul request ke arah ini, tanya: "ini meningkatkan kemungkinan dua orang asing kenalan, atau cuma memudahkan transaksi?" Kalau jawabannya yang kedua → tolak / tunda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konteks positioning (referensi cepat)
|
||||||
|
|
||||||
|
**Untuk siapa:** orang yang ingin pergi tapi tidak punya teman, ingin kenalan baru lewat aktivitas bareng.
|
||||||
|
|
||||||
|
**Bukan untuk:** orang yang sudah punya grup dan tinggal cari paket trip termurah.
|
||||||
|
|
||||||
|
**Categories yang valid** (semua harus punya: organizer, group kecil, interaksi sosial):
|
||||||
|
- Core: hiking, camping
|
||||||
|
- Natural expansion: snorkeling, diving, island hopping
|
||||||
|
- Social activity: city trip, kulineran, konser bareng
|
||||||
|
- Semi-professional: workshop, kelas outdoor, retreat
|
||||||
|
|
||||||
|
**Tagline:** "Pergi bareng, bukan sendiri" / "From strangers to travel buddies".
|
||||||
@@ -25,7 +25,7 @@ export type User = Prisma.UserModel
|
|||||||
/**
|
/**
|
||||||
* Model UserProfile
|
* Model UserProfile
|
||||||
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||||
* (bio, kota, minat). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
* (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||||
*/
|
*/
|
||||||
export type UserProfile = Prisma.UserProfileModel
|
export type UserProfile = Prisma.UserProfileModel
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type User = Prisma.UserModel
|
|||||||
/**
|
/**
|
||||||
* Model UserProfile
|
* Model UserProfile
|
||||||
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||||
* (bio, kota, minat). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
* (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||||
*/
|
*/
|
||||||
export type UserProfile = Prisma.UserProfileModel
|
export type UserProfile = Prisma.UserProfileModel
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -148,6 +148,23 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EnumVibeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.Vibe | Prisma.EnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel> | $Enums.Vibe | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumVibeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.Vibe | Prisma.EnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumVibeNullableWithAggregatesFilter<$PrismaModel> | $Enums.Vibe | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type IntNullableFilter<$PrismaModel = never> = {
|
export type IntNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
@@ -417,6 +434,23 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedEnumVibeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.Vibe | Prisma.EnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel> | $Enums.Vibe | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumVibeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.Vibe | Prisma.EnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.Vibe[] | Prisma.ListEnumVibeFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumVibeNullableWithAggregatesFilter<$PrismaModel> | $Enums.Vibe | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumVibeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
|
|||||||
@@ -9,6 +9,15 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const Vibe = {
|
||||||
|
CHILL: 'CHILL',
|
||||||
|
BALANCED: 'BALANCED',
|
||||||
|
HARDCORE: 'HARDCORE'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Vibe = (typeof Vibe)[keyof typeof Vibe]
|
||||||
|
|
||||||
|
|
||||||
export const VerificationStatus = {
|
export const VerificationStatus = {
|
||||||
PENDING: 'PENDING',
|
PENDING: 'PENDING',
|
||||||
APPROVED: 'APPROVED',
|
APPROVED: 'APPROVED',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1065,6 +1065,7 @@ export const UserProfileScalarFieldEnum = {
|
|||||||
city: 'city',
|
city: 'city',
|
||||||
interests: 'interests',
|
interests: 'interests',
|
||||||
instagram: 'instagram',
|
instagram: 'instagram',
|
||||||
|
vibe: 'vibe',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -1130,6 +1131,7 @@ export const TripScalarFieldEnum = {
|
|||||||
endDate: 'endDate',
|
endDate: 'endDate',
|
||||||
maxParticipants: 'maxParticipants',
|
maxParticipants: 'maxParticipants',
|
||||||
price: 'price',
|
price: 'price',
|
||||||
|
vibe: 'vibe',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
@@ -1241,6 +1243,20 @@ export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Vibe'
|
||||||
|
*/
|
||||||
|
export type EnumVibeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Vibe'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Vibe[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumVibeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Vibe[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'Int'
|
* Reference to a field of type 'Int'
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const UserProfileScalarFieldEnum = {
|
|||||||
city: 'city',
|
city: 'city',
|
||||||
interests: 'interests',
|
interests: 'interests',
|
||||||
instagram: 'instagram',
|
instagram: 'instagram',
|
||||||
|
vibe: 'vibe',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -165,6 +166,7 @@ export const TripScalarFieldEnum = {
|
|||||||
endDate: 'endDate',
|
endDate: 'endDate',
|
||||||
maxParticipants: 'maxParticipants',
|
maxParticipants: 'maxParticipants',
|
||||||
price: 'price',
|
price: 'price',
|
||||||
|
vibe: 'vibe',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type TripMinAggregateOutputType = {
|
|||||||
endDate: Date | null
|
endDate: Date | null
|
||||||
maxParticipants: number | null
|
maxParticipants: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
status: $Enums.TripStatus | null
|
status: $Enums.TripStatus | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -72,6 +73,7 @@ export type TripMaxAggregateOutputType = {
|
|||||||
endDate: Date | null
|
endDate: Date | null
|
||||||
maxParticipants: number | null
|
maxParticipants: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
status: $Enums.TripStatus | null
|
status: $Enums.TripStatus | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -93,6 +95,7 @@ export type TripCountAggregateOutputType = {
|
|||||||
endDate: number
|
endDate: number
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe: number
|
||||||
status: number
|
status: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
@@ -126,6 +129,7 @@ export type TripMinAggregateInputType = {
|
|||||||
endDate?: true
|
endDate?: true
|
||||||
maxParticipants?: true
|
maxParticipants?: true
|
||||||
price?: true
|
price?: true
|
||||||
|
vibe?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -147,6 +151,7 @@ export type TripMaxAggregateInputType = {
|
|||||||
endDate?: true
|
endDate?: true
|
||||||
maxParticipants?: true
|
maxParticipants?: true
|
||||||
price?: true
|
price?: true
|
||||||
|
vibe?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -168,6 +173,7 @@ export type TripCountAggregateInputType = {
|
|||||||
endDate?: true
|
endDate?: true
|
||||||
maxParticipants?: true
|
maxParticipants?: true
|
||||||
price?: true
|
price?: true
|
||||||
|
vibe?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -276,6 +282,7 @@ export type TripGroupByOutputType = {
|
|||||||
endDate: Date | null
|
endDate: Date | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
status: $Enums.TripStatus
|
status: $Enums.TripStatus
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -320,6 +327,7 @@ export type TripWhereInput = {
|
|||||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||||
price?: Prisma.IntFilter<"Trip"> | number
|
price?: Prisma.IntFilter<"Trip"> | number
|
||||||
|
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
@@ -345,6 +353,7 @@ export type TripOrderByWithRelationInput = {
|
|||||||
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
maxParticipants?: Prisma.SortOrder
|
maxParticipants?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -373,6 +382,7 @@ export type TripWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||||
price?: Prisma.IntFilter<"Trip"> | number
|
price?: Prisma.IntFilter<"Trip"> | number
|
||||||
|
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
@@ -398,6 +408,7 @@ export type TripOrderByWithAggregationInput = {
|
|||||||
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
maxParticipants?: Prisma.SortOrder
|
maxParticipants?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -427,6 +438,7 @@ export type TripScalarWhereWithAggregatesInput = {
|
|||||||
endDate?: Prisma.DateTimeNullableWithAggregatesFilter<"Trip"> | Date | string | null
|
endDate?: Prisma.DateTimeNullableWithAggregatesFilter<"Trip"> | Date | string | null
|
||||||
maxParticipants?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
maxParticipants?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
||||||
price?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
price?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
||||||
|
vibe?: Prisma.EnumVibeNullableWithAggregatesFilter<"Trip"> | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusWithAggregatesFilter<"Trip"> | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusWithAggregatesFilter<"Trip"> | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
||||||
@@ -448,6 +460,7 @@ export type TripCreateInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -472,6 +485,7 @@ export type TripUncheckedCreateInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -496,6 +510,7 @@ export type TripUpdateInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -520,6 +535,7 @@ export type TripUncheckedUpdateInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -544,6 +560,7 @@ export type TripCreateManyInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -565,6 +582,7 @@ export type TripUpdateManyMutationInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -585,6 +603,7 @@ export type TripUncheckedUpdateManyInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -616,6 +635,7 @@ export type TripCountOrderByAggregateInput = {
|
|||||||
endDate?: Prisma.SortOrder
|
endDate?: Prisma.SortOrder
|
||||||
maxParticipants?: Prisma.SortOrder
|
maxParticipants?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -642,6 +662,7 @@ export type TripMaxOrderByAggregateInput = {
|
|||||||
endDate?: Prisma.SortOrder
|
endDate?: Prisma.SortOrder
|
||||||
maxParticipants?: Prisma.SortOrder
|
maxParticipants?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -663,6 +684,7 @@ export type TripMinOrderByAggregateInput = {
|
|||||||
endDate?: Prisma.SortOrder
|
endDate?: Prisma.SortOrder
|
||||||
maxParticipants?: Prisma.SortOrder
|
maxParticipants?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -794,6 +816,7 @@ export type TripCreateWithoutOrganizerInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -817,6 +840,7 @@ export type TripUncheckedCreateWithoutOrganizerInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -869,6 +893,7 @@ export type TripScalarWhereInput = {
|
|||||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||||
price?: Prisma.IntFilter<"Trip"> | number
|
price?: Prisma.IntFilter<"Trip"> | number
|
||||||
|
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||||
@@ -890,6 +915,7 @@ export type TripCreateWithoutReviewsInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -913,6 +939,7 @@ export type TripUncheckedCreateWithoutReviewsInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -952,6 +979,7 @@ export type TripUpdateWithoutReviewsInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -975,6 +1003,7 @@ export type TripUncheckedUpdateWithoutReviewsInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -998,6 +1027,7 @@ export type TripCreateWithoutImagesInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -1021,6 +1051,7 @@ export type TripUncheckedCreateWithoutImagesInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -1060,6 +1091,7 @@ export type TripUpdateWithoutImagesInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1083,6 +1115,7 @@ export type TripUncheckedUpdateWithoutImagesInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1106,6 +1139,7 @@ export type TripCreateWithoutParticipantsInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -1129,6 +1163,7 @@ export type TripUncheckedCreateWithoutParticipantsInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -1168,6 +1203,7 @@ export type TripUpdateWithoutParticipantsInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1191,6 +1227,7 @@ export type TripUncheckedUpdateWithoutParticipantsInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1214,6 +1251,7 @@ export type TripCreateManyOrganizerInput = {
|
|||||||
endDate?: Date | string | null
|
endDate?: Date | string | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
status?: $Enums.TripStatus
|
status?: $Enums.TripStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -1234,6 +1272,7 @@ export type TripUpdateWithoutOrganizerInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1257,6 +1296,7 @@ export type TripUncheckedUpdateWithoutOrganizerInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1280,6 +1320,7 @@ export type TripUncheckedUpdateManyWithoutOrganizerInput = {
|
|||||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -1349,6 +1390,7 @@ export type TripSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
endDate?: boolean
|
endDate?: boolean
|
||||||
maxParticipants?: boolean
|
maxParticipants?: boolean
|
||||||
price?: boolean
|
price?: boolean
|
||||||
|
vibe?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -1375,6 +1417,7 @@ export type TripSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
endDate?: boolean
|
endDate?: boolean
|
||||||
maxParticipants?: boolean
|
maxParticipants?: boolean
|
||||||
price?: boolean
|
price?: boolean
|
||||||
|
vibe?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -1397,6 +1440,7 @@ export type TripSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
endDate?: boolean
|
endDate?: boolean
|
||||||
maxParticipants?: boolean
|
maxParticipants?: boolean
|
||||||
price?: boolean
|
price?: boolean
|
||||||
|
vibe?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -1419,13 +1463,14 @@ export type TripSelectScalar = {
|
|||||||
endDate?: boolean
|
endDate?: boolean
|
||||||
maxParticipants?: boolean
|
maxParticipants?: boolean
|
||||||
price?: boolean
|
price?: boolean
|
||||||
|
vibe?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
organizerId?: boolean
|
organizerId?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TripOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "title" | "description" | "category" | "destination" | "location" | "meetingPoint" | "itinerary" | "whatsIncluded" | "whatsExcluded" | "date" | "endDate" | "maxParticipants" | "price" | "status" | "createdAt" | "updatedAt" | "organizerId", ExtArgs["result"]["trip"]>
|
export type TripOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "title" | "description" | "category" | "destination" | "location" | "meetingPoint" | "itinerary" | "whatsIncluded" | "whatsExcluded" | "date" | "endDate" | "maxParticipants" | "price" | "vibe" | "status" | "createdAt" | "updatedAt" | "organizerId", ExtArgs["result"]["trip"]>
|
||||||
export type TripInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type TripInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
||||||
@@ -1481,6 +1526,10 @@ export type $TripPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
endDate: Date | null
|
endDate: Date | null
|
||||||
maxParticipants: number
|
maxParticipants: number
|
||||||
price: number
|
price: number
|
||||||
|
/**
|
||||||
|
* Ritme/energi trip — dipakai untuk matching dengan vibe user.
|
||||||
|
*/
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
status: $Enums.TripStatus
|
status: $Enums.TripStatus
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -1926,6 +1975,7 @@ export interface TripFieldRefs {
|
|||||||
readonly endDate: Prisma.FieldRef<"Trip", 'DateTime'>
|
readonly endDate: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||||
readonly maxParticipants: Prisma.FieldRef<"Trip", 'Int'>
|
readonly maxParticipants: Prisma.FieldRef<"Trip", 'Int'>
|
||||||
readonly price: Prisma.FieldRef<"Trip", 'Int'>
|
readonly price: Prisma.FieldRef<"Trip", 'Int'>
|
||||||
|
readonly vibe: Prisma.FieldRef<"Trip", 'Vibe'>
|
||||||
readonly status: Prisma.FieldRef<"Trip", 'TripStatus'>
|
readonly status: Prisma.FieldRef<"Trip", 'TripStatus'>
|
||||||
readonly createdAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type * as Prisma from "../internal/prismaNamespace"
|
|||||||
/**
|
/**
|
||||||
* Model UserProfile
|
* Model UserProfile
|
||||||
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||||
* (bio, kota, minat). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
* (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||||
*/
|
*/
|
||||||
export type UserProfileModel = runtime.Types.Result.DefaultSelection<Prisma.$UserProfilePayload>
|
export type UserProfileModel = runtime.Types.Result.DefaultSelection<Prisma.$UserProfilePayload>
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ export type UserProfileMinAggregateOutputType = {
|
|||||||
bio: string | null
|
bio: string | null
|
||||||
city: string | null
|
city: string | null
|
||||||
instagram: string | null
|
instagram: string | null
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export type UserProfileMaxAggregateOutputType = {
|
|||||||
bio: string | null
|
bio: string | null
|
||||||
city: string | null
|
city: string | null
|
||||||
instagram: string | null
|
instagram: string | null
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,7 @@ export type UserProfileCountAggregateOutputType = {
|
|||||||
city: number
|
city: number
|
||||||
interests: number
|
interests: number
|
||||||
instagram: number
|
instagram: number
|
||||||
|
vibe: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -64,6 +67,7 @@ export type UserProfileMinAggregateInputType = {
|
|||||||
bio?: true
|
bio?: true
|
||||||
city?: true
|
city?: true
|
||||||
instagram?: true
|
instagram?: true
|
||||||
|
vibe?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -74,6 +78,7 @@ export type UserProfileMaxAggregateInputType = {
|
|||||||
bio?: true
|
bio?: true
|
||||||
city?: true
|
city?: true
|
||||||
instagram?: true
|
instagram?: true
|
||||||
|
vibe?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -85,6 +90,7 @@ export type UserProfileCountAggregateInputType = {
|
|||||||
city?: true
|
city?: true
|
||||||
interests?: true
|
interests?: true
|
||||||
instagram?: true
|
instagram?: true
|
||||||
|
vibe?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -169,6 +175,7 @@ export type UserProfileGroupByOutputType = {
|
|||||||
city: string | null
|
city: string | null
|
||||||
interests: string[]
|
interests: string[]
|
||||||
instagram: string | null
|
instagram: string | null
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: UserProfileCountAggregateOutputType | null
|
_count: UserProfileCountAggregateOutputType | null
|
||||||
@@ -201,6 +208,7 @@ export type UserProfileWhereInput = {
|
|||||||
city?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
city?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
||||||
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
||||||
instagram?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
instagram?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
||||||
|
vibe?: Prisma.EnumVibeNullableFilter<"UserProfile"> | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
||||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
@@ -213,6 +221,7 @@ export type UserProfileOrderByWithRelationInput = {
|
|||||||
city?: Prisma.SortOrderInput | Prisma.SortOrder
|
city?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
interests?: Prisma.SortOrder
|
interests?: Prisma.SortOrder
|
||||||
instagram?: Prisma.SortOrderInput | Prisma.SortOrder
|
instagram?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
user?: Prisma.UserOrderByWithRelationInput
|
user?: Prisma.UserOrderByWithRelationInput
|
||||||
@@ -228,6 +237,7 @@ export type UserProfileWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
city?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
city?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
||||||
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
||||||
instagram?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
instagram?: Prisma.StringNullableFilter<"UserProfile"> | string | null
|
||||||
|
vibe?: Prisma.EnumVibeNullableFilter<"UserProfile"> | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"UserProfile"> | Date | string
|
||||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
@@ -240,6 +250,7 @@ export type UserProfileOrderByWithAggregationInput = {
|
|||||||
city?: Prisma.SortOrderInput | Prisma.SortOrder
|
city?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
interests?: Prisma.SortOrder
|
interests?: Prisma.SortOrder
|
||||||
instagram?: Prisma.SortOrderInput | Prisma.SortOrder
|
instagram?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.UserProfileCountOrderByAggregateInput
|
_count?: Prisma.UserProfileCountOrderByAggregateInput
|
||||||
@@ -257,6 +268,7 @@ export type UserProfileScalarWhereWithAggregatesInput = {
|
|||||||
city?: Prisma.StringNullableWithAggregatesFilter<"UserProfile"> | string | null
|
city?: Prisma.StringNullableWithAggregatesFilter<"UserProfile"> | string | null
|
||||||
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
interests?: Prisma.StringNullableListFilter<"UserProfile">
|
||||||
instagram?: Prisma.StringNullableWithAggregatesFilter<"UserProfile"> | string | null
|
instagram?: Prisma.StringNullableWithAggregatesFilter<"UserProfile"> | string | null
|
||||||
|
vibe?: Prisma.EnumVibeNullableWithAggregatesFilter<"UserProfile"> | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"UserProfile"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"UserProfile"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UserProfile"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UserProfile"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -267,6 +279,7 @@ export type UserProfileCreateInput = {
|
|||||||
city?: string | null
|
city?: string | null
|
||||||
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
||||||
instagram?: string | null
|
instagram?: string | null
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
user: Prisma.UserCreateNestedOneWithoutProfileInput
|
user: Prisma.UserCreateNestedOneWithoutProfileInput
|
||||||
@@ -279,6 +292,7 @@ export type UserProfileUncheckedCreateInput = {
|
|||||||
city?: string | null
|
city?: string | null
|
||||||
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
||||||
instagram?: string | null
|
instagram?: string | null
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -289,6 +303,7 @@ export type UserProfileUpdateInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
|
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
|
||||||
@@ -301,6 +316,7 @@ export type UserProfileUncheckedUpdateInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -312,6 +328,7 @@ export type UserProfileCreateManyInput = {
|
|||||||
city?: string | null
|
city?: string | null
|
||||||
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
||||||
instagram?: string | null
|
instagram?: string | null
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -322,6 +339,7 @@ export type UserProfileUpdateManyMutationInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -333,6 +351,7 @@ export type UserProfileUncheckedUpdateManyInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -357,6 +376,7 @@ export type UserProfileCountOrderByAggregateInput = {
|
|||||||
city?: Prisma.SortOrder
|
city?: Prisma.SortOrder
|
||||||
interests?: Prisma.SortOrder
|
interests?: Prisma.SortOrder
|
||||||
instagram?: Prisma.SortOrder
|
instagram?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -367,6 +387,7 @@ export type UserProfileMaxOrderByAggregateInput = {
|
|||||||
bio?: Prisma.SortOrder
|
bio?: Prisma.SortOrder
|
||||||
city?: Prisma.SortOrder
|
city?: Prisma.SortOrder
|
||||||
instagram?: Prisma.SortOrder
|
instagram?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -377,6 +398,7 @@ export type UserProfileMinOrderByAggregateInput = {
|
|||||||
bio?: Prisma.SortOrder
|
bio?: Prisma.SortOrder
|
||||||
city?: Prisma.SortOrder
|
city?: Prisma.SortOrder
|
||||||
instagram?: Prisma.SortOrder
|
instagram?: Prisma.SortOrder
|
||||||
|
vibe?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -422,12 +444,17 @@ export type UserProfileUpdateinterestsInput = {
|
|||||||
push?: string | string[]
|
push?: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NullableEnumVibeFieldUpdateOperationsInput = {
|
||||||
|
set?: $Enums.Vibe | null
|
||||||
|
}
|
||||||
|
|
||||||
export type UserProfileCreateWithoutUserInput = {
|
export type UserProfileCreateWithoutUserInput = {
|
||||||
id?: string
|
id?: string
|
||||||
bio?: string | null
|
bio?: string | null
|
||||||
city?: string | null
|
city?: string | null
|
||||||
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
||||||
instagram?: string | null
|
instagram?: string | null
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -438,6 +465,7 @@ export type UserProfileUncheckedCreateWithoutUserInput = {
|
|||||||
city?: string | null
|
city?: string | null
|
||||||
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
interests?: Prisma.UserProfileCreateinterestsInput | string[]
|
||||||
instagram?: string | null
|
instagram?: string | null
|
||||||
|
vibe?: $Enums.Vibe | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -464,6 +492,7 @@ export type UserProfileUpdateWithoutUserInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -474,6 +503,7 @@ export type UserProfileUncheckedUpdateWithoutUserInput = {
|
|||||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
interests?: Prisma.UserProfileUpdateinterestsInput | string[]
|
||||||
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
instagram?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -487,6 +517,7 @@ export type UserProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
city?: boolean
|
city?: boolean
|
||||||
interests?: boolean
|
interests?: boolean
|
||||||
instagram?: boolean
|
instagram?: boolean
|
||||||
|
vibe?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
@@ -499,6 +530,7 @@ export type UserProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
city?: boolean
|
city?: boolean
|
||||||
interests?: boolean
|
interests?: boolean
|
||||||
instagram?: boolean
|
instagram?: boolean
|
||||||
|
vibe?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
@@ -511,6 +543,7 @@ export type UserProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
city?: boolean
|
city?: boolean
|
||||||
interests?: boolean
|
interests?: boolean
|
||||||
instagram?: boolean
|
instagram?: boolean
|
||||||
|
vibe?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
@@ -523,11 +556,12 @@ export type UserProfileSelectScalar = {
|
|||||||
city?: boolean
|
city?: boolean
|
||||||
interests?: boolean
|
interests?: boolean
|
||||||
instagram?: boolean
|
instagram?: boolean
|
||||||
|
vibe?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "bio" | "city" | "interests" | "instagram" | "createdAt" | "updatedAt", ExtArgs["result"]["userProfile"]>
|
export type UserProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "bio" | "city" | "interests" | "instagram" | "vibe" | "createdAt" | "updatedAt", ExtArgs["result"]["userProfile"]>
|
||||||
export type UserProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
@@ -562,6 +596,10 @@ export type $UserProfilePayload<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
* Username Instagram (tanpa @, opsional)
|
* Username Instagram (tanpa @, opsional)
|
||||||
*/
|
*/
|
||||||
instagram: string | null
|
instagram: string | null
|
||||||
|
/**
|
||||||
|
* Gaya jalan / energi user — dipakai untuk matching teman dengan ritme serupa.
|
||||||
|
*/
|
||||||
|
vibe: $Enums.Vibe | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["userProfile"]>
|
}, ExtArgs["result"]["userProfile"]>
|
||||||
@@ -994,6 +1032,7 @@ export interface UserProfileFieldRefs {
|
|||||||
readonly city: Prisma.FieldRef<"UserProfile", 'String'>
|
readonly city: Prisma.FieldRef<"UserProfile", 'String'>
|
||||||
readonly interests: Prisma.FieldRef<"UserProfile", 'String[]'>
|
readonly interests: Prisma.FieldRef<"UserProfile", 'String[]'>
|
||||||
readonly instagram: Prisma.FieldRef<"UserProfile", 'String'>
|
readonly instagram: Prisma.FieldRef<"UserProfile", 'String'>
|
||||||
|
readonly vibe: Prisma.FieldRef<"UserProfile", 'Vibe'>
|
||||||
readonly createdAt: Prisma.FieldRef<"UserProfile", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"UserProfile", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"UserProfile", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"UserProfile", 'DateTime'>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { SessionProvider } from "@/components/providers/session-provider";
|
import { SessionProvider } from "@/components/providers/session-provider";
|
||||||
import { Navbar } from "@/components/shared/navbar";
|
import { Navbar } from "@/components/shared/navbar";
|
||||||
|
import { ProfileNudgeBanner } from "@/components/shared/profile-nudge-banner";
|
||||||
import { siteConfig, siteUrl } from "@/lib/site";
|
import { siteConfig, siteUrl } from "@/lib/site";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ export default function RootLayout({
|
|||||||
<body className="flex min-h-full flex-col bg-neutral-50">
|
<body className="flex min-h-full flex-col bg-neutral-50">
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
<ProfileNudgeBanner />
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+41
-9
@@ -1,11 +1,25 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
|
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
||||||
import { ACTIVITY_CATEGORIES, categoryMeta } from "@/lib/activity-category";
|
import { ACTIVITY_CATEGORIES, categoryMeta } from "@/lib/activity-category";
|
||||||
|
|
||||||
|
type OpenTrip = Awaited<ReturnType<typeof tripService.getOpenTrips>>[number];
|
||||||
|
|
||||||
|
function mapParticipants(trip: OpenTrip) {
|
||||||
|
return trip.participants.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.user.name,
|
||||||
|
image: p.user.image,
|
||||||
|
interests: p.user.profile?.interests ?? [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Cari Teman Trip & Aktivitas — Pergi Bareng, Bukan Sendiri",
|
title: "Cari Teman Trip & Aktivitas — Pergi Bareng, Bukan Sendiri",
|
||||||
description: `${siteConfig.slogan} ${siteConfig.description}`,
|
description: `${siteConfig.slogan} ${siteConfig.description}`,
|
||||||
@@ -18,7 +32,14 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const trips = await tripService.getOpenTrips();
|
const session = await getServerSession(authOptions);
|
||||||
|
const [trips, viewerProfile] = await Promise.all([
|
||||||
|
tripService.getOpenTrips(),
|
||||||
|
session?.user?.id
|
||||||
|
? profileRepo.findByUserId(session.user.id)
|
||||||
|
: Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
const viewerInterests = viewerProfile?.interests ?? [];
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||||
@@ -35,8 +56,10 @@ export default async function HomePage() {
|
|||||||
|
|
||||||
const shownIds = new Set([...upcomingIds, ...latestTrips.map((t) => t.id)]);
|
const shownIds = new Set([...upcomingIds, ...latestTrips.map((t) => t.id)]);
|
||||||
|
|
||||||
const budgetTrips = trips
|
// Section sosial: trip yang paling ramai joiner-nya (social proof, bukan price proof).
|
||||||
.filter((t) => !shownIds.has(t.id) && t.price <= 300000)
|
const buzzingTrips = trips
|
||||||
|
.filter((t) => !shownIds.has(t.id) && t._count.participants > 0)
|
||||||
|
.sort((a, b) => b._count.participants - a._count.participants)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
const orgJsonLd = {
|
const orgJsonLd = {
|
||||||
@@ -191,6 +214,7 @@ export default async function HomePage() {
|
|||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
@@ -204,6 +228,8 @@ export default async function HomePage() {
|
|||||||
isVerifiedOrganizer={
|
isVerifiedOrganizer={
|
||||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||||
}
|
}
|
||||||
|
participants={mapParticipants(trip)}
|
||||||
|
viewerInterests={viewerInterests}
|
||||||
priority={i === 0}
|
priority={i === 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -261,6 +287,7 @@ export default async function HomePage() {
|
|||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
@@ -274,35 +301,38 @@ export default async function HomePage() {
|
|||||||
isVerifiedOrganizer={
|
isVerifiedOrganizer={
|
||||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||||
}
|
}
|
||||||
|
participants={mapParticipants(trip)}
|
||||||
|
viewerInterests={viewerInterests}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Budget Friendly */}
|
{/* Lagi Ramai — social proof, bukan price proof */}
|
||||||
{budgetTrips.length > 0 && (
|
{buzzingTrips.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="mb-4 flex items-center gap-3 sm:mb-5">
|
<div className="mb-4 flex items-center gap-3 sm:mb-5">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100 text-base sm:h-9 sm:w-9 sm:text-lg">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100 text-base sm:h-9 sm:w-9 sm:text-lg">
|
||||||
💸
|
🤝
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
|
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
|
||||||
Budget Friendly
|
Lagi Ramai
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
||||||
Trip di bawah Rp 300.000
|
Banyak yang sudah gabung — kamu nggak bakal jalan sendirian
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{budgetTrips.map((trip) => (
|
{buzzingTrips.map((trip) => (
|
||||||
<TripCard
|
<TripCard
|
||||||
key={trip.id}
|
key={trip.id}
|
||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
@@ -316,6 +346,8 @@ export default async function HomePage() {
|
|||||||
isVerifiedOrganizer={
|
isVerifiedOrganizer={
|
||||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||||
}
|
}
|
||||||
|
participants={mapParticipants(trip)}
|
||||||
|
viewerInterests={viewerInterests}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { profileService } from "@/server/services/profile.service";
|
||||||
|
import { UserCard } from "@/features/profile/components/user-card";
|
||||||
|
import { PeopleFilter } from "@/features/profile/components/people-filter";
|
||||||
|
import { isVibe, vibeLabel } from "@/lib/vibe";
|
||||||
|
import { siteConfig } from "@/lib/site";
|
||||||
|
|
||||||
|
interface PeoplePageProps {
|
||||||
|
searchParams: Promise<{
|
||||||
|
city?: string;
|
||||||
|
interest?: string;
|
||||||
|
vibe?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
searchParams,
|
||||||
|
}: PeoplePageProps): Promise<Metadata> {
|
||||||
|
const { city, interest, vibe: vibeParam } = await searchParams;
|
||||||
|
const vibe = isVibe(vibeParam) ? vibeParam : undefined;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (vibe) parts.push(`Vibe ${vibeLabel(vibe).toLowerCase()}`);
|
||||||
|
if (city) parts.push(`di ${city}`);
|
||||||
|
if (interest) parts.push(`#${interest.toLowerCase()}`);
|
||||||
|
const title = parts.length
|
||||||
|
? `Cari Teman ${parts.join(" ")}`
|
||||||
|
: "Cari Teman Aktivitas — Profil Anggota";
|
||||||
|
const description = `Telusuri profil anggota ${siteConfig.name} berdasarkan minat, kota, dan vibe. Temukan calon teman trip dengan ritme yang cocok sebelum gabung bareng.`;
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: "/people" },
|
||||||
|
openGraph: { title, description, url: "/people" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PeoplePage({ searchParams }: PeoplePageProps) {
|
||||||
|
const params = await searchParams;
|
||||||
|
const vibe = isVibe(params.vibe) ? params.vibe : undefined;
|
||||||
|
const filters = {
|
||||||
|
city: params.city?.trim() || undefined,
|
||||||
|
interest: params.interest?.trim().toLowerCase() || undefined,
|
||||||
|
vibe,
|
||||||
|
};
|
||||||
|
const hasFilters = Boolean(filters.city || filters.interest || filters.vibe);
|
||||||
|
|
||||||
|
const people = await profileService.findPeople(filters);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-6 sm:py-8">
|
||||||
|
<div className="mb-5 flex flex-col gap-2 sm:mb-6">
|
||||||
|
<h1 className="text-xl font-bold text-neutral-800 sm:text-2xl">
|
||||||
|
Cari Teman Aktivitas
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
{hasFilters
|
||||||
|
? `${people.length} orang ditemukan dengan filter di atas`
|
||||||
|
: `${people.length} anggota dengan profil sosial — kenali dulu sebelum gabung trip`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PeopleFilter />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{people.length === 0 ? (
|
||||||
|
<div className="rounded-2xl border-2 border-dashed border-neutral-200 bg-white p-8 text-center sm:p-14">
|
||||||
|
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary-50 text-2xl sm:h-16 sm:w-16 sm:text-3xl">
|
||||||
|
🔍
|
||||||
|
</div>
|
||||||
|
<p className="mb-1 text-base font-bold text-neutral-800 sm:text-lg">
|
||||||
|
{hasFilters
|
||||||
|
? "Belum ada anggota yang cocok"
|
||||||
|
: "Belum ada anggota dengan profil terisi"}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
{hasFilters
|
||||||
|
? "Coba longgarkan filter — kota, minat, atau vibe."
|
||||||
|
: "Setelah anggota lain mengisi profil, mereka akan muncul di sini."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{people.map((u) => (
|
||||||
|
<li key={u.id}>
|
||||||
|
<UserCard
|
||||||
|
id={u.id}
|
||||||
|
name={u.name}
|
||||||
|
image={u.image}
|
||||||
|
isVerifiedOrganizer={
|
||||||
|
u.organizerVerification?.status === "APPROVED"
|
||||||
|
}
|
||||||
|
profile={u.profile}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ export default async function ProfilePage() {
|
|||||||
city: ownProfile.city,
|
city: ownProfile.city,
|
||||||
interests: ownProfile.interests,
|
interests: ownProfile.interests,
|
||||||
instagram: ownProfile.instagram,
|
instagram: ownProfile.instagram,
|
||||||
|
vibe: ownProfile.vibe,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,7 @@ export default async function ProfilePage() {
|
|||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
|
|||||||
+79
-19
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { trustService } from "@/server/services/trust.service";
|
import { trustService } from "@/server/services/trust.service";
|
||||||
@@ -15,6 +16,8 @@ import { TripProgramBlock } from "@/features/trip/components/trip-program-block"
|
|||||||
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
||||||
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 { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||||
|
import { categoryMeta } from "@/lib/activity-category";
|
||||||
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
import {
|
import {
|
||||||
isPastTripLastDayForReview,
|
isPastTripLastDayForReview,
|
||||||
isTripDepartureDayPast,
|
isTripDepartureDayPast,
|
||||||
@@ -127,6 +130,8 @@ export default async function TripDetailPage({
|
|||||||
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const catMeta = categoryMeta(trip.category);
|
||||||
|
|
||||||
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
||||||
const eventStatus =
|
const eventStatus =
|
||||||
trip.status === "OPEN"
|
trip.status === "OPEN"
|
||||||
@@ -240,8 +245,21 @@ export default async function TripDetailPage({
|
|||||||
<h1 className="text-lg font-bold text-neutral-800 sm:text-xl">
|
<h1 className="text-lg font-bold text-neutral-800 sm:text-xl">
|
||||||
{trip.title}
|
{trip.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-0.5 flex items-center gap-1.5 text-sm text-neutral-500">
|
<p className="mt-0.5 flex flex-wrap items-center gap-1.5 text-sm text-neutral-500">
|
||||||
🏔️ {trip.destination}
|
<span aria-hidden>{catMeta.icon}</span>
|
||||||
|
<span className="rounded-full bg-neutral-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||||
|
{catMeta.label}
|
||||||
|
</span>
|
||||||
|
{trip.vibe && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-0.5 rounded-full bg-secondary-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-secondary-700"
|
||||||
|
title={vibeMeta(trip.vibe).description}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{vibeMeta(trip.vibe).icon}</span>
|
||||||
|
<span>{vibeMeta(trip.vibe).label}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="truncate">{trip.destination}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
@@ -448,9 +466,12 @@ export default async function TripDetailPage({
|
|||||||
|
|
||||||
{/* Peserta yang sudah disetujui organizer (publik) */}
|
{/* Peserta yang sudah disetujui organizer (publik) */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-3 text-xs font-bold text-neutral-700 sm:text-sm">
|
<h2 className="mb-1 text-xs font-bold text-neutral-700 sm:text-sm">
|
||||||
Peserta terkonfirmasi ({confirmedCount})
|
Peserta terkonfirmasi ({confirmedCount})
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mb-3 text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
Kenalan dulu sebelum berangkat — klik kartu untuk lihat profil.
|
||||||
|
</p>
|
||||||
{confirmedCount === 0 ? (
|
{confirmedCount === 0 ? (
|
||||||
<p className="text-xs text-neutral-400 sm:text-sm">
|
<p className="text-xs text-neutral-400 sm:text-sm">
|
||||||
Belum ada peserta yang dikonfirmasi.{" "}
|
Belum ada peserta yang dikonfirmasi.{" "}
|
||||||
@@ -459,22 +480,61 @@ export default async function TripDetailPage({
|
|||||||
: "Jadilah yang pertama mendaftar! 🎒"}
|
: "Jadilah yang pertama mendaftar! 🎒"}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
<ul className="grid gap-2 sm:grid-cols-2">
|
||||||
{confirmedParticipants.map((p) => (
|
{confirmedParticipants.map((p) => {
|
||||||
<Link
|
const interests = p.user.profile?.interests ?? [];
|
||||||
key={p.id}
|
const city = p.user.profile?.city;
|
||||||
href={`/u/${p.user.id}`}
|
return (
|
||||||
className="flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 transition-colors hover:bg-primary-100 sm:gap-2 sm:px-3 sm:py-1.5"
|
<li key={p.id}>
|
||||||
>
|
<Link
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-600 text-[9px] font-bold text-white sm:h-6 sm:w-6 sm:text-[10px]">
|
href={`/u/${p.user.id}`}
|
||||||
{p.user.name.charAt(0).toUpperCase()}
|
className="flex items-start gap-3 rounded-xl border border-neutral-200 bg-white px-3 py-2.5 transition-colors hover:border-primary-300 hover:bg-primary-50/30"
|
||||||
</div>
|
>
|
||||||
<span className="text-xs font-medium text-neutral-700 sm:text-sm">
|
{p.user.image ? (
|
||||||
{p.user.name}
|
<Image
|
||||||
</span>
|
src={p.user.image}
|
||||||
</Link>
|
alt=""
|
||||||
))}
|
width={40}
|
||||||
</div>
|
height={40}
|
||||||
|
className="h-10 w-10 shrink-0 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary-600 text-sm font-bold text-white">
|
||||||
|
{p.user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-semibold text-neutral-800">
|
||||||
|
{p.user.name}
|
||||||
|
</p>
|
||||||
|
{city && (
|
||||||
|
<p className="truncate text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
📍 {city}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{interests.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{interests.slice(0, 3).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="rounded-full bg-secondary-50 px-1.5 py-0.5 text-[10px] font-medium text-secondary-700"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{interests.length > 3 && (
|
||||||
|
<span className="text-[10px] text-neutral-400">
|
||||||
|
+{interests.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+33
-2
@@ -1,11 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
|
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { TripFilter } from "@/features/trip/components/trip-filter";
|
import { TripFilter } from "@/features/trip/components/trip-filter";
|
||||||
import { siteConfig } from "@/lib/site";
|
import { siteConfig } from "@/lib/site";
|
||||||
import { categoryLabel, isActivityCategory } from "@/lib/activity-category";
|
import { categoryLabel, isActivityCategory } from "@/lib/activity-category";
|
||||||
|
import { isVibe } from "@/lib/vibe";
|
||||||
|
import type { GroupSize } from "@/server/repositories/trip.repo";
|
||||||
|
|
||||||
|
const GROUP_SIZES: GroupSize[] = ["SMALL", "MEDIUM", "LARGE"];
|
||||||
|
function isGroupSize(value: unknown): value is GroupSize {
|
||||||
|
return typeof value === "string" && (GROUP_SIZES as string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
interface TripsPageProps {
|
interface TripsPageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
@@ -13,6 +23,8 @@ interface TripsPageProps {
|
|||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
vibe?: string;
|
||||||
|
groupSize?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,19 +56,30 @@ export async function generateMetadata({
|
|||||||
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const category = isActivityCategory(params.category) ? params.category : undefined;
|
const category = isActivityCategory(params.category) ? params.category : undefined;
|
||||||
const hasFilters = Boolean(params.q || params.from || params.to || category);
|
const vibe = isVibe(params.vibe) ? params.vibe : undefined;
|
||||||
|
const groupSize = isGroupSize(params.groupSize) ? params.groupSize : undefined;
|
||||||
|
const hasFilters = Boolean(
|
||||||
|
params.q || params.from || params.to || category || vibe || groupSize
|
||||||
|
);
|
||||||
const filters = {
|
const filters = {
|
||||||
q: params.q,
|
q: params.q,
|
||||||
from: params.from,
|
from: params.from,
|
||||||
to: params.to,
|
to: params.to,
|
||||||
category,
|
category,
|
||||||
|
vibe,
|
||||||
|
groupSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [trips, allTrips] = await Promise.all([
|
const session = await getServerSession(authOptions);
|
||||||
|
const [trips, allTrips, viewerProfile] = await Promise.all([
|
||||||
tripService.getOpenTrips(filters),
|
tripService.getOpenTrips(filters),
|
||||||
hasFilters ? tripService.getOpenTrips() : null,
|
hasFilters ? tripService.getOpenTrips() : null,
|
||||||
|
session?.user?.id
|
||||||
|
? profileRepo.findByUserId(session.user.id)
|
||||||
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
const totalCount = hasFilters ? allTrips!.length : trips.length;
|
const totalCount = hasFilters ? allTrips!.length : trips.length;
|
||||||
|
const viewerInterests = viewerProfile?.interests ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 sm:py-8">
|
<div className="mx-auto max-w-6xl px-4 py-6 sm:py-8">
|
||||||
@@ -120,6 +143,7 @@ export default async function TripsPage({ searchParams }: TripsPageProps) {
|
|||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
@@ -133,6 +157,13 @@ export default async function TripsPage({ searchParams }: TripsPageProps) {
|
|||||||
isVerifiedOrganizer={
|
isVerifiedOrganizer={
|
||||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||||
}
|
}
|
||||||
|
participants={trip.participants.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.user.name,
|
||||||
|
image: p.user.image,
|
||||||
|
interests: p.user.profile?.interests ?? [],
|
||||||
|
}))}
|
||||||
|
viewerInterests={viewerInterests}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { profileService } from "@/server/services/profile.service";
|
|||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||||
import { siteConfig } from "@/lib/site";
|
import { siteConfig } from "@/lib/site";
|
||||||
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
@@ -94,6 +95,18 @@ export default async function PublicProfilePage({ params }: PageProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{profile?.vibe && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full bg-primary-50 px-2.5 py-1 text-xs font-semibold text-primary-700"
|
||||||
|
title={vibeMeta(profile.vibe).description}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{vibeMeta(profile.vibe).icon}</span>
|
||||||
|
<span>Vibe: {vibeMeta(profile.vibe).label}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{profile?.interests && profile.interests.length > 0 && (
|
{profile?.interests && profile.interests.length > 0 && (
|
||||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||||
{profile.interests.map((tag) => (
|
{profile.interests.map((tag) => (
|
||||||
@@ -164,6 +177,7 @@ export default async function PublicProfilePage({ params }: PageProps) {
|
|||||||
id={trip.id}
|
id={trip.id}
|
||||||
title={trip.title}
|
title={trip.title}
|
||||||
category={trip.category}
|
category={trip.category}
|
||||||
|
vibe={trip.vibe}
|
||||||
destination={trip.destination}
|
destination={trip.destination}
|
||||||
location={trip.location}
|
location={trip.location}
|
||||||
date={trip.date}
|
date={trip.date}
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ export function Navbar() {
|
|||||||
>
|
>
|
||||||
Open Trip
|
Open Trip
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/people"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Cari Teman
|
||||||
|
</Link>
|
||||||
|
|
||||||
{session?.user ? (
|
{session?.user ? (
|
||||||
<>
|
<>
|
||||||
@@ -125,6 +131,13 @@ export function Navbar() {
|
|||||||
>
|
>
|
||||||
Open Trip
|
Open Trip
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/people"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="rounded-lg px-3 py-2.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50"
|
||||||
|
>
|
||||||
|
Cari Teman
|
||||||
|
</Link>
|
||||||
|
|
||||||
{session?.user ? (
|
{session?.user ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server component banner: muncul di atas semua halaman ketika user sudah login
|
||||||
|
* tapi profil sosialnya kosong. Menjaga janji "kenalan dulu, gabung kemudian"
|
||||||
|
* dengan mendorong user mengisi minat/kota sebelum join trip.
|
||||||
|
*/
|
||||||
|
export async function ProfileNudgeBanner() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) return null;
|
||||||
|
|
||||||
|
const profile = await profileRepo.findByUserId(session.user.id);
|
||||||
|
const hasMeaningfulProfile =
|
||||||
|
!!profile &&
|
||||||
|
(!!profile.bio?.trim() ||
|
||||||
|
!!profile.city?.trim() ||
|
||||||
|
profile.interests.length > 0);
|
||||||
|
|
||||||
|
if (hasMeaningfulProfile) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b border-amber-200 bg-amber-50">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col items-start gap-2 px-4 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between sm:text-sm">
|
||||||
|
<p className="text-amber-900">
|
||||||
|
<span className="font-semibold">Lengkapi profil sosial kamu</span> —
|
||||||
|
bio, kota, dan minat. Calon teman trip akan lebih mudah kenal kamu
|
||||||
|
sebelum gabung bareng.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="shrink-0 rounded-lg bg-amber-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
Isi profil sekarang
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
-136
@@ -1,136 +0,0 @@
|
|||||||
# Deploy Setrip dengan PM2
|
|
||||||
|
|
||||||
Panduan ini untuk menjalankan aplikasi **Next.js** (setrip) di server Linux/VPS menggunakan **PM2**. Pastikan **PostgreSQL** sudah tersedia dan URL-nya sesuai dengan variabel lingkungan aplikasi.
|
|
||||||
|
|
||||||
## Prasyarat
|
|
||||||
|
|
||||||
- Node.js **20.x** (disarankan, selaras dengan `@types/node` di proyek)
|
|
||||||
- npm atau pnpm/yarn (contoh di bawah memakai **npm**)
|
|
||||||
- PM2 terpasang global: `npm install -g pm2`
|
|
||||||
- Basis data PostgreSQL dan file `.env` di server (lihat bagian Lingkungan)
|
|
||||||
|
|
||||||
## File PM2
|
|
||||||
|
|
||||||
Konfigurasi PM2 ada di root repositori: **`ecosystem.config.js`** (nama ini disengaja).
|
|
||||||
|
|
||||||
### Jangan `pm2 start ecosystem.js` kecuali itu skrip Node
|
|
||||||
|
|
||||||
Jika Anda menjalankan `pm2 start ecosystem.js` pada file yang isinya hanya `module.exports = { apps: [...] }`, PM2 menganggapnya **skrip aplikasi biasa** dan menjalankannya dengan `node ecosystem.js`. Akibatnya:
|
|
||||||
|
|
||||||
- Nama proses di daftar PM2 jadi **`ecosystem`** (bukan `setrip`).
|
|
||||||
- Next.js **tidak** dijalankan lewat entri `apps` Anda.
|
|
||||||
|
|
||||||
Gunakan selalu:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
```
|
|
||||||
|
|
||||||
Isi file menjalankan biner Next (`next start`) setelah build, mode **fork**, satu proses, **PORT** **3090**. Ubah `PORT` di file tersebut jika kebijakan port berubah.
|
|
||||||
|
|
||||||
### Berapa port yang dibutuhkan?
|
|
||||||
|
|
||||||
Untuk **trafik HTTP/HTTPS ke aplikasi Next.js**, cukup **satu port** yang didengarkan oleh `next start` — di setup ini **3090** (atau satu port lain yang Anda set).
|
|
||||||
|
|
||||||
**PostgreSQL** memakai port tersendiri (biasanya **5432**) di mesin tempat database berjalan. Itu bukan “port kedua untuk publik” dari aplikasi web: koneksi DB terjadi dari server aplikasi ke database (localhost atau jaringan internal). Di firewall publik Anda biasanya hanya membuka **80/443** (reverse proxy) atau **3090** jika diakses langsung tanpa proxy.
|
|
||||||
|
|
||||||
## Langkah deploy (pertama kali)
|
|
||||||
|
|
||||||
1. **Clone** repositori ke server (misalnya `/var/www/setrip`).
|
|
||||||
|
|
||||||
2. **Masuk** ke folder proyek dan pasang dependensi produksi:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/setrip
|
|
||||||
npm ci
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Lingkungan** — salin atau buat `.env` / `.env.production` di server (jangan commit rahasia ke git). Minimal sesuai kebutuhan aplikasi Anda, contoh:
|
|
||||||
|
|
||||||
- `DATABASE_URL` — koneksi PostgreSQL
|
|
||||||
- `NEXTAUTH_SECRET` — string acak yang kuat
|
|
||||||
- `NEXTAUTH_URL` — URL publik aplikasi (harus cocok dengan yang dibuka browser), misalnya `https://domain-anda.com` atau `http://host:3090` jika tanpa HTTPS dan akses langsung ke port tersebut
|
|
||||||
|
|
||||||
4. **Prisma** — generate client dan terapkan migrasi (jika memakai migrasi):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prisma generate
|
|
||||||
npx prisma migrate deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Build** Next.js:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Mulai** dengan PM2:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
```
|
|
||||||
|
|
||||||
Tanpa `--env production` tetap jalan; variabel default memakai blok `env` di file.
|
|
||||||
|
|
||||||
7. **Simpan** daftar proses agar bangkit lagi setelah reboot:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 save
|
|
||||||
pm2 startup
|
|
||||||
```
|
|
||||||
|
|
||||||
Ikuti perintah yang dikeluarkan PM2 (biasanya menyalin satu baris `sudo env ...`).
|
|
||||||
|
|
||||||
## Perintah PM2 yang sering dipakai
|
|
||||||
|
|
||||||
| Perintah | Keterangan |
|
|
||||||
|----------|------------|
|
|
||||||
| `pm2 status` | Status semua aplikasi |
|
|
||||||
| `pm2 logs setrip` | Log aplikasi bernama `setrip` |
|
|
||||||
| `pm2 reload setrip` | Reload tanpa downtime (berguna setelah deploy baru) |
|
|
||||||
| `pm2 restart setrip` | Restart proses |
|
|
||||||
| `pm2 stop setrip` | Menghentikan aplikasi |
|
|
||||||
| `pm2 delete setrip` | Menghapus aplikasi dari daftar PM2 |
|
|
||||||
|
|
||||||
## Deploy ulang (update kode)
|
|
||||||
|
|
||||||
Di server, setelah `git pull` (atau salin artefak baru):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/setrip
|
|
||||||
npm ci
|
|
||||||
npx prisma generate
|
|
||||||
npx prisma migrate deploy
|
|
||||||
npm run build
|
|
||||||
pm2 reload setrip
|
|
||||||
```
|
|
||||||
|
|
||||||
Jika nama aplikasi di PM2 berbeda, ganti `setrip` dengan nama di `ecosystem.config.js` (`name`).
|
|
||||||
|
|
||||||
### Hapus proses PM2 yang salah (nama `ecosystem`)
|
|
||||||
|
|
||||||
Jika Anda pernah menjalankan `pm2 start ecosystem.js` dan muncul proses bernama `ecosystem`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 stop ecosystem
|
|
||||||
pm2 delete ecosystem
|
|
||||||
```
|
|
||||||
|
|
||||||
Atau pakai id dari `pm2 status` (contoh id `9`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 stop 9
|
|
||||||
pm2 delete 9
|
|
||||||
```
|
|
||||||
|
|
||||||
Lalu mulai lagi dengan `pm2 start ecosystem.config.js --env production` dan `pm2 save`.
|
|
||||||
|
|
||||||
## Reverse proxy (opsional)
|
|
||||||
|
|
||||||
Agar bisa HTTPS dan port 80/443, letakkan **Nginx** (atau Caddy) di depan aplikasi yang mendengarkan di `127.0.0.1:3090`. Pastikan `NEXTAUTH_URL` memakai skema dan host yang sama dengan yang diakses pengguna.
|
|
||||||
|
|
||||||
## Pemecahan masalah
|
|
||||||
|
|
||||||
- **502 / tidak terhubung** — cek `pm2 logs setrip`, pastikan PostgreSQL dapat dijangkau dari server, dan `PORT` tidak bentrok dengan layanan lain.
|
|
||||||
- **Error Prisma** — pastikan `npx prisma generate` dijalankan setelah `npm ci` di setiap deploy, dan `DATABASE_URL` benar.
|
|
||||||
- **NextAuth** — `NEXTAUTH_URL` harus persis URL publik (termasuk `https://`).
|
|
||||||
@@ -22,6 +22,7 @@ export async function updateProfileAction(formData: FormData) {
|
|||||||
city: formData.get("city"),
|
city: formData.get("city"),
|
||||||
instagram: formData.get("instagram"),
|
instagram: formData.get("instagram"),
|
||||||
interests,
|
interests,
|
||||||
|
vibe: formData.get("vibe"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = updateProfileSchema.safeParse(raw);
|
const parsed = updateProfileSchema.safeParse(raw);
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { VIBES, vibeMeta, isVibe } from "@/lib/vibe";
|
||||||
|
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
export function PeopleFilter() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const initialVibe = searchParams.get("vibe");
|
||||||
|
const [vibe, setVibe] = useState<Vibe | null>(
|
||||||
|
isVibe(initialVibe) ? initialVibe : null
|
||||||
|
);
|
||||||
|
const [city, setCity] = useState(searchParams.get("city") ?? "");
|
||||||
|
const [interest, setInterest] = useState(searchParams.get("interest") ?? "");
|
||||||
|
|
||||||
|
function buildParams(overrides?: { vibe?: Vibe | null }) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const nextVibe = overrides && "vibe" in overrides ? overrides.vibe : vibe;
|
||||||
|
if (nextVibe) params.set("vibe", nextVibe);
|
||||||
|
if (city.trim()) params.set("city", city.trim());
|
||||||
|
if (interest.trim()) params.set("interest", interest.trim().toLowerCase());
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushFilters(params: URLSearchParams) {
|
||||||
|
const qs = params.toString();
|
||||||
|
router.push(`/people${qs ? `?${qs}` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectVibe(next: Vibe | null) {
|
||||||
|
setVibe(next);
|
||||||
|
pushFilters(buildParams({ vibe: next }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
pushFilters(buildParams());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
setVibe(null);
|
||||||
|
setCity("");
|
||||||
|
setInterest("");
|
||||||
|
router.push("/people");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFilters = vibe || city.trim() || interest.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSearch}
|
||||||
|
className="rounded-2xl border border-neutral-200 bg-white p-4 shadow-sm sm:p-5"
|
||||||
|
>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
||||||
|
Vibe
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectVibe(null)}
|
||||||
|
aria-pressed={vibe === null}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
vibe === null
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
{VIBES.map((v) => {
|
||||||
|
const m = vibeMeta(v);
|
||||||
|
const active = vibe === v;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectVibe(v)}
|
||||||
|
aria-pressed={active}
|
||||||
|
title={m.description}
|
||||||
|
className={`inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{m.icon}</span>
|
||||||
|
<span>{m.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label
|
||||||
|
htmlFor="people-city"
|
||||||
|
className="mb-1.5 block text-xs font-medium text-neutral-500"
|
||||||
|
>
|
||||||
|
Kota
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="people-city"
|
||||||
|
type="text"
|
||||||
|
value={city}
|
||||||
|
onChange={(e) => setCity(e.target.value)}
|
||||||
|
placeholder="Bandung, Jakarta, ..."
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:border-primary-500 focus:bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label
|
||||||
|
htmlFor="people-interest"
|
||||||
|
className="mb-1.5 block text-xs font-medium text-neutral-500"
|
||||||
|
>
|
||||||
|
Minat
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="people-interest"
|
||||||
|
type="text"
|
||||||
|
value={interest}
|
||||||
|
onChange={(e) => setInterest(e.target.value)}
|
||||||
|
placeholder="hiking, fotografi, yoga..."
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:border-primary-500 focus:bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 sm:shrink-0">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 rounded-xl bg-primary-600 px-5 py-2.5 text-sm font-semibold text-white shadow-md shadow-primary-600/20 transition-colors hover:bg-primary-700 sm:flex-none"
|
||||||
|
>
|
||||||
|
Cari
|
||||||
|
</button>
|
||||||
|
{hasFilters && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="rounded-xl border border-neutral-200 px-3 py-2.5 text-sm font-medium text-neutral-500 transition-colors hover:bg-neutral-50 hover:text-neutral-700"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { updateProfileAction } from "@/features/profile/actions";
|
import { updateProfileAction } from "@/features/profile/actions";
|
||||||
import { LIMITS } from "@/lib/limits";
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
import { VIBES, vibeMeta } from "@/lib/vibe";
|
||||||
|
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
interface ProfileEditorProps {
|
interface ProfileEditorProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -12,6 +14,7 @@ interface ProfileEditorProps {
|
|||||||
city: string | null;
|
city: string | null;
|
||||||
interests: string[];
|
interests: string[];
|
||||||
instagram: string | null;
|
instagram: string | null;
|
||||||
|
vibe: Vibe | null;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@ export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
|||||||
const [instagram, setInstagram] = useState(initial?.instagram ?? "");
|
const [instagram, setInstagram] = useState(initial?.instagram ?? "");
|
||||||
const [interests, setInterests] = useState<string[]>(initial?.interests ?? []);
|
const [interests, setInterests] = useState<string[]>(initial?.interests ?? []);
|
||||||
const [interestDraft, setInterestDraft] = useState("");
|
const [interestDraft, setInterestDraft] = useState("");
|
||||||
|
const [vibe, setVibe] = useState<Vibe | null>(initial?.vibe ?? null);
|
||||||
|
|
||||||
function addInterest() {
|
function addInterest() {
|
||||||
const v = interestDraft.trim().toLowerCase();
|
const v = interestDraft.trim().toLowerCase();
|
||||||
@@ -65,6 +69,7 @@ export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
|||||||
if (bio.trim()) formData.set("bio", bio.trim());
|
if (bio.trim()) formData.set("bio", bio.trim());
|
||||||
if (city.trim()) formData.set("city", city.trim());
|
if (city.trim()) formData.set("city", city.trim());
|
||||||
if (instagram.trim()) formData.set("instagram", instagram.trim());
|
if (instagram.trim()) formData.set("instagram", instagram.trim());
|
||||||
|
if (vibe) formData.set("vibe", vibe);
|
||||||
interests.forEach((t) => formData.append("interests", t));
|
interests.forEach((t) => formData.append("interests", t));
|
||||||
|
|
||||||
const result = await updateProfileAction(formData);
|
const result = await updateProfileAction(formData);
|
||||||
@@ -257,6 +262,56 @@ export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||||
|
Vibe jalanmu{" "}
|
||||||
|
<span className="text-xs font-normal text-neutral-400">(opsional)</span>
|
||||||
|
</label>
|
||||||
|
<p className="mb-2 text-[11px] text-neutral-500">
|
||||||
|
Bantu calon teman trip nyambung dengan ritme kamu.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVibe(null)}
|
||||||
|
aria-pressed={vibe === null}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
vibe === null
|
||||||
|
? "border-neutral-700 bg-neutral-800 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Belum diisi
|
||||||
|
</button>
|
||||||
|
{VIBES.map((v) => {
|
||||||
|
const m = vibeMeta(v);
|
||||||
|
const active = vibe === v;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVibe(v)}
|
||||||
|
aria-pressed={active}
|
||||||
|
title={m.description}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{m.icon}</span>
|
||||||
|
<span>{m.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{vibe && (
|
||||||
|
<p className="mt-2 text-[11px] italic text-neutral-500">
|
||||||
|
{vibeMeta(vibe).description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
|
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
interface UserCardProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string | null;
|
||||||
|
isVerifiedOrganizer: boolean;
|
||||||
|
profile: {
|
||||||
|
bio: string | null;
|
||||||
|
city: string | null;
|
||||||
|
interests: string[];
|
||||||
|
vibe: Vibe | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserCard({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
image,
|
||||||
|
isVerifiedOrganizer,
|
||||||
|
profile,
|
||||||
|
}: UserCardProps) {
|
||||||
|
const interests = profile?.interests ?? [];
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/u/${id}`}
|
||||||
|
className="group flex h-full flex-col rounded-2xl border border-neutral-200 bg-white p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary-300 hover:shadow-md"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{image ? (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
className="h-14 w-14 shrink-0 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-primary-600 text-lg font-bold text-white">
|
||||||
|
{name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-bold text-neutral-800 group-hover:text-primary-700">
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
{profile?.city && (
|
||||||
|
<p className="truncate text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
📍 {profile.city}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{isVerifiedOrganizer && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-0.5 rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary-800"
|
||||||
|
title="Organizer terverifikasi"
|
||||||
|
>
|
||||||
|
✅ Organizer
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{profile?.vibe && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-0.5 rounded-full bg-secondary-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-secondary-800"
|
||||||
|
title={vibeMeta(profile.vibe).description}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{vibeMeta(profile.vibe).icon}</span>
|
||||||
|
<span>{vibeMeta(profile.vibe).label}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{profile?.bio && (
|
||||||
|
<p className="mt-3 line-clamp-2 text-xs leading-relaxed text-neutral-600">
|
||||||
|
{profile.bio}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{interests.length > 0 && (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-1">
|
||||||
|
{interests.slice(0, 5).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="rounded-full bg-secondary-50 px-2 py-0.5 text-[10px] font-medium text-secondary-700"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{interests.length > 5 && (
|
||||||
|
<span className="text-[10px] text-neutral-400">
|
||||||
|
+{interests.length - 5}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { LIMITS } from "@/lib/limits";
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
import { VIBES } from "@/lib/vibe";
|
||||||
|
|
||||||
const optionalTrimmed = (max: number, label: string) =>
|
const optionalTrimmed = (max: number, label: string) =>
|
||||||
z.preprocess(
|
z.preprocess(
|
||||||
@@ -52,6 +53,14 @@ export const updateProfileSchema = z.object({
|
|||||||
`Maksimal ${LIMITS.MAX_PROFILE_INTERESTS_COUNT} minat`
|
`Maksimal ${LIMITS.MAX_PROFILE_INTERESTS_COUNT} minat`
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
|
vibe: z.preprocess(
|
||||||
|
(val) => {
|
||||||
|
if (val == null) return undefined;
|
||||||
|
const s = String(val).trim();
|
||||||
|
return s === "" ? undefined : s;
|
||||||
|
},
|
||||||
|
z.enum([...VIBES]).optional()
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function createTripAction(formData: FormData) {
|
|||||||
endDate: (formData.get("endDate") as string) || undefined,
|
endDate: (formData.get("endDate") as string) || undefined,
|
||||||
maxParticipants: formData.get("maxParticipants") as string,
|
maxParticipants: formData.get("maxParticipants") as string,
|
||||||
price: formData.get("price") as string,
|
price: formData.get("price") as string,
|
||||||
|
vibe: formData.get("vibe"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = createTripSchema.safeParse(raw);
|
const result = createTripSchema.safeParse(raw);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
ACTIVITY_CATEGORIES,
|
ACTIVITY_CATEGORIES,
|
||||||
categoryMeta,
|
categoryMeta,
|
||||||
} from "@/lib/activity-category";
|
} from "@/lib/activity-category";
|
||||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import { VIBES, vibeMeta } from "@/lib/vibe";
|
||||||
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
function formatRupiahInput(value: string): string {
|
function formatRupiahInput(value: string): string {
|
||||||
const num = value.replace(/\D/g, "");
|
const num = value.replace(/\D/g, "");
|
||||||
@@ -32,6 +33,7 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [category, setCategory] = useState<ActivityCategory>("HIKING");
|
const [category, setCategory] = useState<ActivityCategory>("HIKING");
|
||||||
|
const [vibe, setVibe] = useState<Vibe | null>(null);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
const [priceDisplay, setPriceDisplay] = useState("");
|
const [priceDisplay, setPriceDisplay] = useState("");
|
||||||
@@ -62,6 +64,7 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
formData.set("price", parseRupiahInput(priceDisplay));
|
formData.set("price", parseRupiahInput(priceDisplay));
|
||||||
|
if (vibe) formData.set("vibe", vibe);
|
||||||
|
|
||||||
const result = await createTripAction(formData);
|
const result = await createTripAction(formData);
|
||||||
|
|
||||||
@@ -124,6 +127,57 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
|||||||
<input type="hidden" name="category" value={category} />
|
<input type="hidden" name="category" value={category} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Vibe Chips */}
|
||||||
|
<div className="rounded-xl bg-secondary-50 p-4">
|
||||||
|
<label className="mb-1 block text-sm font-bold text-secondary-900">
|
||||||
|
Vibe Trip{" "}
|
||||||
|
<span className="text-xs font-normal text-secondary-700">(opsional)</span>
|
||||||
|
</label>
|
||||||
|
<p className="mb-2 text-[11px] text-secondary-700/80">
|
||||||
|
Bantu calon peserta menilai apakah ritmenya cocok dengan mereka.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVibe(null)}
|
||||||
|
aria-pressed={vibe === null}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
vibe === null
|
||||||
|
? "border-neutral-700 bg-neutral-800 text-white"
|
||||||
|
: "border-secondary-200 bg-white text-secondary-800 hover:bg-secondary-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Belum diisi
|
||||||
|
</button>
|
||||||
|
{VIBES.map((v) => {
|
||||||
|
const m = vibeMeta(v);
|
||||||
|
const active = v === vibe;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVibe(v)}
|
||||||
|
aria-pressed={active}
|
||||||
|
title={m.description}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-secondary-600 bg-secondary-600 text-white"
|
||||||
|
: "border-secondary-200 bg-white text-secondary-800 hover:bg-secondary-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{m.icon}</span>
|
||||||
|
<span>{m.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{vibe && (
|
||||||
|
<p className="mt-2 text-[11px] italic text-secondary-700/80">
|
||||||
|
{vibeMeta(vibe).description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="title" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
<label htmlFor="title" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||||
Judul Trip
|
Judul Trip
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import Link from "next/link";
|
|||||||
import { formatRupiah } from "@/lib/utils";
|
import { formatRupiah } from "@/lib/utils";
|
||||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||||
import { categoryMeta } from "@/lib/activity-category";
|
import { categoryMeta } from "@/lib/activity-category";
|
||||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
interface TripCardParticipant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string | null;
|
||||||
|
interests: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface TripCardProps {
|
interface TripCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
category: ActivityCategory;
|
category: ActivityCategory;
|
||||||
|
vibe?: Vibe | null;
|
||||||
destination: string;
|
destination: string;
|
||||||
location: string;
|
location: string;
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
@@ -21,12 +30,17 @@ interface TripCardProps {
|
|||||||
coverImage?: string | null;
|
coverImage?: string | null;
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
isVerifiedOrganizer?: boolean;
|
isVerifiedOrganizer?: boolean;
|
||||||
|
/** Daftar peserta CONFIRMED (subset, untuk preview avatar). Optional. */
|
||||||
|
participants?: TripCardParticipant[];
|
||||||
|
/** Interests user yang sedang melihat — untuk hitung overlap. Optional. */
|
||||||
|
viewerInterests?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TripCard({
|
export function TripCard({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
category,
|
category,
|
||||||
|
vibe,
|
||||||
destination,
|
destination,
|
||||||
location,
|
location,
|
||||||
date,
|
date,
|
||||||
@@ -39,10 +53,25 @@ export function TripCard({
|
|||||||
coverImage,
|
coverImage,
|
||||||
priority,
|
priority,
|
||||||
isVerifiedOrganizer,
|
isVerifiedOrganizer,
|
||||||
|
participants,
|
||||||
|
viewerInterests,
|
||||||
}: TripCardProps) {
|
}: TripCardProps) {
|
||||||
const spotsLeft = maxParticipants - participantCount;
|
const spotsLeft = maxParticipants - participantCount;
|
||||||
const isSmallGroup = maxParticipants <= 10;
|
const isSmallGroup = maxParticipants <= 10;
|
||||||
const meta = categoryMeta(category);
|
const meta = categoryMeta(category);
|
||||||
|
const vMeta = vibe ? vibeMeta(vibe) : null;
|
||||||
|
|
||||||
|
const previewParticipants = participants?.slice(0, 3) ?? [];
|
||||||
|
const moreCount =
|
||||||
|
participants && participants.length > 3 ? participants.length - 3 : 0;
|
||||||
|
|
||||||
|
let overlapCount = 0;
|
||||||
|
if (viewerInterests && viewerInterests.length > 0 && participants) {
|
||||||
|
const viewerSet = new Set(viewerInterests.map((i) => i.toLowerCase()));
|
||||||
|
overlapCount = participants.filter((p) =>
|
||||||
|
p.interests.some((tag) => viewerSet.has(tag.toLowerCase()))
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/trips/${id}`} className="group block">
|
<Link href={`/trips/${id}`} className="group block">
|
||||||
@@ -63,13 +92,24 @@ export function TripCard({
|
|||||||
<span className="text-4xl">{meta.icon}</span>
|
<span className="text-4xl">{meta.icon}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span
|
<div className="absolute left-3 top-3 flex flex-wrap gap-1">
|
||||||
className="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-semibold text-neutral-700 shadow-sm backdrop-blur-sm"
|
<span
|
||||||
title={`Kategori: ${meta.label}`}
|
className="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-semibold text-neutral-700 shadow-sm backdrop-blur-sm"
|
||||||
>
|
title={`Kategori: ${meta.label}`}
|
||||||
<span>{meta.icon}</span>
|
>
|
||||||
<span>{meta.label}</span>
|
<span>{meta.icon}</span>
|
||||||
</span>
|
<span>{meta.label}</span>
|
||||||
|
</span>
|
||||||
|
{vMeta && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 rounded-full bg-secondary-600/90 px-2 py-0.5 text-[11px] font-semibold text-white shadow-sm backdrop-blur-sm"
|
||||||
|
title={`Vibe: ${vMeta.label} — ${vMeta.description}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{vMeta.icon}</span>
|
||||||
|
<span>{vMeta.label}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`absolute right-3 top-3 rounded-full px-2.5 py-0.5 text-xs font-bold backdrop-blur-sm ${
|
className={`absolute right-3 top-3 rounded-full px-2.5 py-0.5 text-xs font-bold backdrop-blur-sm ${
|
||||||
status === "OPEN"
|
status === "OPEN"
|
||||||
@@ -120,6 +160,48 @@ export function TripCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(previewParticipants.length > 0 || overlapCount > 0) && (
|
||||||
|
<div className="mt-3 flex items-center gap-2 border-t border-neutral-100 pt-3">
|
||||||
|
{previewParticipants.length > 0 && (
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{previewParticipants.map((p) =>
|
||||||
|
p.image ? (
|
||||||
|
<Image
|
||||||
|
key={p.id}
|
||||||
|
src={p.image}
|
||||||
|
alt=""
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="h-6 w-6 rounded-full border-2 border-white object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-primary-600 text-[10px] font-bold text-white"
|
||||||
|
title={p.name}
|
||||||
|
>
|
||||||
|
{p.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{moreCount > 0 && (
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-neutral-200 text-[10px] font-bold text-neutral-600">
|
||||||
|
+{moreCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{overlapCount > 0 && (
|
||||||
|
<span
|
||||||
|
className="rounded-full bg-secondary-50 px-2 py-0.5 text-[11px] font-semibold text-secondary-700"
|
||||||
|
title="Peserta dengan minimal 1 minat sama dengan kamu"
|
||||||
|
>
|
||||||
|
✨ {overlapCount} peserta sama minat
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-3 flex items-center justify-between border-t border-neutral-100 pt-3">
|
<div className="mt-3 flex items-center justify-between border-t border-neutral-100 pt-3">
|
||||||
<span className="text-lg font-bold text-primary-600">
|
<span className="text-lg font-bold text-primary-600">
|
||||||
{formatRupiah(price)}
|
{formatRupiah(price)}
|
||||||
|
|||||||
@@ -10,16 +10,38 @@ import {
|
|||||||
categoryMeta,
|
categoryMeta,
|
||||||
isActivityCategory,
|
isActivityCategory,
|
||||||
} from "@/lib/activity-category";
|
} from "@/lib/activity-category";
|
||||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import { VIBES, vibeMeta, isVibe } from "@/lib/vibe";
|
||||||
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
type GroupSize = "SMALL" | "MEDIUM" | "LARGE";
|
||||||
|
const GROUP_SIZES: { value: GroupSize; label: string; hint: string }[] = [
|
||||||
|
{ value: "SMALL", label: "Small", hint: "≤10 — paling akrab" },
|
||||||
|
{ value: "MEDIUM", label: "Medium", hint: "11–20" },
|
||||||
|
{ value: "LARGE", label: "Large", hint: "21+" },
|
||||||
|
];
|
||||||
|
function isGroupSize(value: unknown): value is GroupSize {
|
||||||
|
return (
|
||||||
|
typeof value === "string" &&
|
||||||
|
GROUP_SIZES.some((g) => g.value === value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function TripFilter() {
|
export function TripFilter() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const initialCategory = searchParams.get("category");
|
const initialCategory = searchParams.get("category");
|
||||||
|
const initialVibe = searchParams.get("vibe");
|
||||||
|
const initialGroup = searchParams.get("groupSize");
|
||||||
const [category, setCategory] = useState<ActivityCategory | null>(
|
const [category, setCategory] = useState<ActivityCategory | null>(
|
||||||
isActivityCategory(initialCategory) ? initialCategory : null
|
isActivityCategory(initialCategory) ? initialCategory : null
|
||||||
);
|
);
|
||||||
|
const [vibe, setVibe] = useState<Vibe | null>(
|
||||||
|
isVibe(initialVibe) ? initialVibe : null
|
||||||
|
);
|
||||||
|
const [groupSize, setGroupSize] = useState<GroupSize | null>(
|
||||||
|
isGroupSize(initialGroup) ? initialGroup : null
|
||||||
|
);
|
||||||
const [query, setQuery] = useState(searchParams.get("q") ?? "");
|
const [query, setQuery] = useState(searchParams.get("q") ?? "");
|
||||||
const [startDate, setStartDate] = useState<Date | null>(
|
const [startDate, setStartDate] = useState<Date | null>(
|
||||||
searchParams.get("from") ? new Date(searchParams.get("from")!) : null
|
searchParams.get("from") ? new Date(searchParams.get("from")!) : null
|
||||||
@@ -28,11 +50,20 @@ export function TripFilter() {
|
|||||||
searchParams.get("to") ? new Date(searchParams.get("to")!) : null
|
searchParams.get("to") ? new Date(searchParams.get("to")!) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
function buildParams(overrides?: { category?: ActivityCategory | null }) {
|
function buildParams(overrides?: {
|
||||||
|
category?: ActivityCategory | null;
|
||||||
|
vibe?: Vibe | null;
|
||||||
|
groupSize?: GroupSize | null;
|
||||||
|
}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const nextCategory =
|
const nextCategory =
|
||||||
overrides && "category" in overrides ? overrides.category : category;
|
overrides && "category" in overrides ? overrides.category : category;
|
||||||
|
const nextVibe = overrides && "vibe" in overrides ? overrides.vibe : vibe;
|
||||||
|
const nextGroup =
|
||||||
|
overrides && "groupSize" in overrides ? overrides.groupSize : groupSize;
|
||||||
if (nextCategory) params.set("category", nextCategory);
|
if (nextCategory) params.set("category", nextCategory);
|
||||||
|
if (nextVibe) params.set("vibe", nextVibe);
|
||||||
|
if (nextGroup) params.set("groupSize", nextGroup);
|
||||||
if (query.trim()) params.set("q", query.trim());
|
if (query.trim()) params.set("q", query.trim());
|
||||||
if (startDate) params.set("from", formatLocalCalendarYmd(startDate));
|
if (startDate) params.set("from", formatLocalCalendarYmd(startDate));
|
||||||
if (endDate) params.set("to", formatLocalCalendarYmd(endDate));
|
if (endDate) params.set("to", formatLocalCalendarYmd(endDate));
|
||||||
@@ -49,6 +80,16 @@ export function TripFilter() {
|
|||||||
pushFilters(buildParams({ category: next }));
|
pushFilters(buildParams({ category: next }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectVibe(next: Vibe | null) {
|
||||||
|
setVibe(next);
|
||||||
|
pushFilters(buildParams({ vibe: next }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectGroupSize(next: GroupSize | null) {
|
||||||
|
setGroupSize(next);
|
||||||
|
pushFilters(buildParams({ groupSize: next }));
|
||||||
|
}
|
||||||
|
|
||||||
function handleDateChange(dates: [Date | null, Date | null]) {
|
function handleDateChange(dates: [Date | null, Date | null]) {
|
||||||
const [start, end] = dates;
|
const [start, end] = dates;
|
||||||
setStartDate(start);
|
setStartDate(start);
|
||||||
@@ -57,6 +98,8 @@ export function TripFilter() {
|
|||||||
if (!start && !end) {
|
if (!start && !end) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (category) params.set("category", category);
|
if (category) params.set("category", category);
|
||||||
|
if (vibe) params.set("vibe", vibe);
|
||||||
|
if (groupSize) params.set("groupSize", groupSize);
|
||||||
if (query.trim()) params.set("q", query.trim());
|
if (query.trim()) params.set("q", query.trim());
|
||||||
pushFilters(params);
|
pushFilters(params);
|
||||||
}
|
}
|
||||||
@@ -69,13 +112,15 @@ export function TripFilter() {
|
|||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setCategory(null);
|
setCategory(null);
|
||||||
|
setVibe(null);
|
||||||
|
setGroupSize(null);
|
||||||
setQuery("");
|
setQuery("");
|
||||||
setStartDate(null);
|
setStartDate(null);
|
||||||
setEndDate(null);
|
setEndDate(null);
|
||||||
router.push("/trips");
|
router.push("/trips");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilters = category || query || startDate || endDate;
|
const hasFilters = category || vibe || groupSize || query || startDate || endDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -123,6 +168,89 @@ export function TripFilter() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Vibe & Ukuran grup */}
|
||||||
|
<div className="mb-4 grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
||||||
|
Vibe
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectVibe(null)}
|
||||||
|
aria-pressed={vibe === null}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
vibe === null
|
||||||
|
? "border-secondary-600 bg-secondary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
{VIBES.map((v) => {
|
||||||
|
const m = vibeMeta(v);
|
||||||
|
const active = vibe === v;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectVibe(v)}
|
||||||
|
aria-pressed={active}
|
||||||
|
title={m.description}
|
||||||
|
className={`inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-secondary-600 bg-secondary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden>{m.icon}</span>
|
||||||
|
<span>{m.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
||||||
|
Ukuran grup
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectGroupSize(null)}
|
||||||
|
aria-pressed={groupSize === null}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
groupSize === null
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
{GROUP_SIZES.map((g) => {
|
||||||
|
const active = groupSize === g.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={g.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectGroupSize(g.value)}
|
||||||
|
aria-pressed={active}
|
||||||
|
title={g.hint}
|
||||||
|
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{g.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-3">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-3">
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { LIMITS } from "@/lib/limits";
|
import { LIMITS } from "@/lib/limits";
|
||||||
import { ACTIVITY_CATEGORIES } from "@/lib/activity-category";
|
import { ACTIVITY_CATEGORIES } from "@/lib/activity-category";
|
||||||
|
import { VIBES } from "@/lib/vibe";
|
||||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
||||||
import {
|
import {
|
||||||
isTripDepartureDayPast,
|
isTripDepartureDayPast,
|
||||||
@@ -146,6 +147,14 @@ export const createTripSchema = z
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
),
|
),
|
||||||
|
vibe: z.preprocess(
|
||||||
|
(val) => {
|
||||||
|
if (val == null) return undefined;
|
||||||
|
const s = String(val).trim();
|
||||||
|
return s === "" ? undefined : s;
|
||||||
|
},
|
||||||
|
z.enum([...VIBES]).optional()
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const dep = tripStoredInstantFromYmd(data.date);
|
const dep = tripStoredInstantFromYmd(data.date);
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
export const VIBES = ["CHILL", "BALANCED", "HARDCORE"] as const satisfies readonly Vibe[];
|
||||||
|
|
||||||
|
interface VibeMeta {
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const META: Record<Vibe, VibeMeta> = {
|
||||||
|
CHILL: {
|
||||||
|
label: "Chill",
|
||||||
|
icon: "😌",
|
||||||
|
description: "Santai, nikmati prosesnya, no rush.",
|
||||||
|
},
|
||||||
|
BALANCED: {
|
||||||
|
label: "Balanced",
|
||||||
|
icon: "🙂",
|
||||||
|
description: "Seimbang — ada effort, ada healing.",
|
||||||
|
},
|
||||||
|
HARDCORE: {
|
||||||
|
label: "Hardcore",
|
||||||
|
icon: "🔥",
|
||||||
|
description: "Push limit, target tercapai, full energy.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function vibeMeta(vibe: Vibe): VibeMeta {
|
||||||
|
return META[vibe];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vibeLabel(vibe: Vibe): string {
|
||||||
|
return META[vibe].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVibe(value: unknown): value is Vibe {
|
||||||
|
return (
|
||||||
|
typeof value === "string" && (VIBES as readonly string[]).includes(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Vibe" AS ENUM ('CHILL', 'BALANCED', 'HARDCORE');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserProfile" ADD COLUMN "vibe" "Vibe";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Trip" ADD COLUMN "vibe" "Vibe";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Trip_vibe_status_date_idx" ON "Trip"("vibe", "status", "date");
|
||||||
+12
-1
@@ -35,7 +35,7 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
/// Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||||
/// (bio, kota, minat). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
/// (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||||
model UserProfile {
|
model UserProfile {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String @unique
|
userId String @unique
|
||||||
@@ -49,11 +49,19 @@ model UserProfile {
|
|||||||
interests String[] @default([])
|
interests String[] @default([])
|
||||||
/// Username Instagram (tanpa @, opsional)
|
/// Username Instagram (tanpa @, opsional)
|
||||||
instagram String?
|
instagram String?
|
||||||
|
/// Gaya jalan / energi user — dipakai untuk matching teman dengan ritme serupa.
|
||||||
|
vibe Vibe?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Vibe {
|
||||||
|
CHILL
|
||||||
|
BALANCED
|
||||||
|
HARDCORE
|
||||||
|
}
|
||||||
|
|
||||||
/// Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
/// Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
||||||
/// Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
|
/// Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
|
||||||
model Account {
|
model Account {
|
||||||
@@ -136,6 +144,8 @@ model Trip {
|
|||||||
endDate DateTime?
|
endDate DateTime?
|
||||||
maxParticipants Int
|
maxParticipants Int
|
||||||
price Int
|
price Int
|
||||||
|
/// Ritme/energi trip — dipakai untuk matching dengan vibe user.
|
||||||
|
vibe Vibe?
|
||||||
status TripStatus @default(OPEN)
|
status TripStatus @default(OPEN)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -148,6 +158,7 @@ model Trip {
|
|||||||
reviews TripReview[]
|
reviews TripReview[]
|
||||||
|
|
||||||
@@index([category, status, date])
|
@@index([category, status, date])
|
||||||
|
@@index([vibe, status, date])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TripReview {
|
model TripReview {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
interface UpsertProfileInput {
|
interface UpsertProfileInput {
|
||||||
bio?: string;
|
bio?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
instagram?: string;
|
instagram?: string;
|
||||||
interests: string[];
|
interests: string[];
|
||||||
|
vibe?: Vibe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const profileRepo = {
|
export const profileRepo = {
|
||||||
@@ -21,12 +23,14 @@ export const profileRepo = {
|
|||||||
city: data.city,
|
city: data.city,
|
||||||
instagram: data.instagram,
|
instagram: data.instagram,
|
||||||
interests: data.interests,
|
interests: data.interests,
|
||||||
|
vibe: data.vibe,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
bio: data.bio,
|
bio: data.bio,
|
||||||
city: data.city,
|
city: data.city,
|
||||||
instagram: data.instagram,
|
instagram: data.instagram,
|
||||||
interests: data.interests,
|
interests: data.interests,
|
||||||
|
vibe: data.vibe,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
||||||
import {
|
import {
|
||||||
utcStartOfDay,
|
utcStartOfDay,
|
||||||
utcDayStartFromYmd,
|
utcDayStartFromYmd,
|
||||||
@@ -8,11 +8,15 @@ import {
|
|||||||
maxUtcDate,
|
maxUtcDate,
|
||||||
} from "@/lib/trip-dates";
|
} from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export type GroupSize = "SMALL" | "MEDIUM" | "LARGE";
|
||||||
|
|
||||||
export interface TripFilters {
|
export interface TripFilters {
|
||||||
q?: string;
|
q?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
category?: ActivityCategory;
|
category?: ActivityCategory;
|
||||||
|
vibe?: Vibe;
|
||||||
|
groupSize?: GroupSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tripRepo = {
|
export const tripRepo = {
|
||||||
@@ -47,6 +51,18 @@ export const tripRepo = {
|
|||||||
andParts.push({ category: filters.category });
|
andParts.push({ category: filters.category });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters?.vibe) {
|
||||||
|
andParts.push({ vibe: filters.vibe });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.groupSize === "SMALL") {
|
||||||
|
andParts.push({ maxParticipants: { lte: 10 } });
|
||||||
|
} else if (filters?.groupSize === "MEDIUM") {
|
||||||
|
andParts.push({ maxParticipants: { gte: 11, lte: 20 } });
|
||||||
|
} else if (filters?.groupSize === "LARGE") {
|
||||||
|
andParts.push({ maxParticipants: { gte: 21 } });
|
||||||
|
}
|
||||||
|
|
||||||
if (!filters?.from && !filters?.to) {
|
if (!filters?.from && !filters?.to) {
|
||||||
andParts.push({ date: { gte: todayStart } });
|
andParts.push({ date: { gte: todayStart } });
|
||||||
} else {
|
} else {
|
||||||
@@ -104,6 +120,22 @@ export const tripRepo = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
images: { orderBy: { order: "asc" }, take: 1 },
|
images: { orderBy: { order: "asc" }, take: 1 },
|
||||||
|
participants: {
|
||||||
|
where: { status: "CONFIRMED" },
|
||||||
|
take: 10,
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
profile: { select: { interests: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
participants: { where: { status: { not: "CANCELLED" } } },
|
participants: { where: { status: { not: "CANCELLED" } } },
|
||||||
@@ -129,7 +161,16 @@ export const tripRepo = {
|
|||||||
},
|
},
|
||||||
images: { orderBy: { order: "asc" } },
|
images: { orderBy: { order: "asc" } },
|
||||||
participants: {
|
participants: {
|
||||||
include: { user: { select: { id: true, name: true, image: true } } },
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
profile: { select: { city: true, interests: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
reviews: {
|
reviews: {
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
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 type { Vibe } from "@/app/generated/prisma/enums";
|
||||||
|
|
||||||
|
export interface PeopleFilters {
|
||||||
|
city?: string;
|
||||||
|
interest?: string;
|
||||||
|
vibe?: Vibe;
|
||||||
|
}
|
||||||
|
|
||||||
export const userRepo = {
|
export const userRepo = {
|
||||||
async findByEmail(email: string) {
|
async findByEmail(email: string) {
|
||||||
@@ -42,6 +49,7 @@ export const userRepo = {
|
|||||||
city: true,
|
city: true,
|
||||||
interests: true,
|
interests: true,
|
||||||
instagram: true,
|
instagram: true,
|
||||||
|
vibe: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
organizerVerification: { select: { status: true } },
|
organizerVerification: { select: { status: true } },
|
||||||
@@ -49,6 +57,56 @@ export const userRepo = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovery /people: ambil user yang punya profil sosial terisi (minimal salah
|
||||||
|
* satu dari bio/city/interests/vibe). Filter optional by city/interest/vibe.
|
||||||
|
* Tidak ekspos email/KYC.
|
||||||
|
*/
|
||||||
|
async findPeople(filters?: PeopleFilters, limit = 60) {
|
||||||
|
const profileWhere: Prisma.UserProfileWhereInput = {
|
||||||
|
OR: [
|
||||||
|
{ bio: { not: null } },
|
||||||
|
{ city: { not: null } },
|
||||||
|
{ vibe: { not: null } },
|
||||||
|
{ interests: { isEmpty: false } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters?.city) {
|
||||||
|
profileWhere.city = {
|
||||||
|
contains: filters.city,
|
||||||
|
mode: "insensitive",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (filters?.interest) {
|
||||||
|
profileWhere.interests = { has: filters.interest.toLowerCase() };
|
||||||
|
}
|
||||||
|
if (filters?.vibe) {
|
||||||
|
profileWhere.vibe = filters.vibe;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.user.findMany({
|
||||||
|
where: { profile: { is: profileWhere } },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
createdAt: true,
|
||||||
|
profile: {
|
||||||
|
select: {
|
||||||
|
bio: true,
|
||||||
|
city: true,
|
||||||
|
interests: true,
|
||||||
|
vibe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
organizerVerification: { select: { status: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async create(data: Prisma.UserCreateInput) {
|
async create(data: Prisma.UserCreateInput) {
|
||||||
return prisma.user.create({ data });
|
return prisma.user.create({ data });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { userRepo } from "@/server/repositories/user.repo";
|
import { userRepo, type PeopleFilters } from "@/server/repositories/user.repo";
|
||||||
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 { organizerRepo } from "@/server/repositories/organizer.repo";
|
import { organizerRepo } from "@/server/repositories/organizer.repo";
|
||||||
@@ -11,12 +11,17 @@ export const profileService = {
|
|||||||
return profileRepo.findByUserId(userId);
|
return profileRepo.findByUserId(userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async findPeople(filters?: PeopleFilters) {
|
||||||
|
return userRepo.findPeople(filters);
|
||||||
|
},
|
||||||
|
|
||||||
async updateProfile(userId: string, input: UpdateProfileInput) {
|
async updateProfile(userId: string, input: UpdateProfileInput) {
|
||||||
return profileRepo.upsertByUserId(userId, {
|
return profileRepo.upsertByUserId(userId, {
|
||||||
bio: input.bio,
|
bio: input.bio,
|
||||||
city: input.city,
|
city: input.city,
|
||||||
instagram: input.instagram,
|
instagram: input.instagram,
|
||||||
interests: input.interests,
|
interests: input.interests,
|
||||||
|
vibe: input.vibe,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Prisma } from "@/app/generated/prisma/client";
|
import { Prisma } from "@/app/generated/prisma/client";
|
||||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
|
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
|
||||||
import { participantRepo } from "@/server/repositories/participant.repo";
|
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||||
@@ -31,6 +31,7 @@ interface CreateTripInput {
|
|||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
maxParticipants: number;
|
maxParticipants: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
vibe?: Vibe;
|
||||||
organizerId: string;
|
organizerId: string;
|
||||||
imageUrls?: string[];
|
imageUrls?: string[];
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ export const tripService = {
|
|||||||
endDate: input.endDate,
|
endDate: input.endDate,
|
||||||
maxParticipants: input.maxParticipants,
|
maxParticipants: input.maxParticipants,
|
||||||
price: input.price,
|
price: input.price,
|
||||||
|
vibe: input.vibe,
|
||||||
organizer: { connect: { id: input.organizerId } },
|
organizer: { connect: { id: input.organizerId } },
|
||||||
images,
|
images,
|
||||||
} satisfies Prisma.TripCreateInput;
|
} satisfies Prisma.TripCreateInput;
|
||||||
|
|||||||
Reference in New Issue
Block a user