Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccb3437e82 | |||
| f5d86d2414 | |||
| 7f419638b5 | |||
| 3228ef712f | |||
| 63349a144d |
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:unsplash.com)"
|
||||
"WebFetch(domain:unsplash.com)",
|
||||
"Bash(npx prisma *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# private uploads (KYC: KTP / selfie). Never serve directly.
|
||||
# private uploads (KYC: KTP / liveness). Never serve directly.
|
||||
/uploads/
|
||||
|
||||
# vercel
|
||||
|
||||
+1
-1
@@ -236,7 +236,7 @@ Alur data mengikuti pola yang sama: **UI (`app/`) → server actions (`features/
|
||||
|
||||
### Verifikasi organizer (KYC ringan)
|
||||
|
||||
- Model `OrganizerVerification` (1-1 ke `User`) menyimpan KTP (nama, NIK unik, tanggal lahir, alamat), URL foto KTP & selfie, data rekening bank, dan status `PENDING` / `APPROVED` / `REJECTED` + audit reviewer.
|
||||
- Model `OrganizerVerification` (1-1 ke `User`) menyimpan KTP (nama, NIK unik, tanggal lahir, alamat), storage key foto KTP & foto liveness (user memegang kertas tulisan "SETRIP" sebagai bukti pengajuan), data rekening bank, dan status `PENDING` / `APPROVED` / `REJECTED` + audit reviewer.
|
||||
- Alur: user submit di `/verify` (`features/organizer/`) → admin review di `/admin/verifications` → setujui/tolak.
|
||||
- **Gate trip berbayar:** `createTripAction` menolak `price > 0` jika user belum `APPROVED` (`organizerService.isApproved`).
|
||||
- **Akses admin:** `lib/admin.ts → isAdminEmail()` membaca `ADMIN_EMAILS` (env, comma-separated).
|
||||
|
||||
@@ -23,7 +23,7 @@ Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tid
|
||||
|
||||
Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini.
|
||||
|
||||
**Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, selfie, dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip.
|
||||
**Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, foto liveness (memegang kertas tulisan "SETRIP"), dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip.
|
||||
|
||||
### 3. Peserta: mencari trip
|
||||
|
||||
@@ -97,7 +97,7 @@ Alur ini menggambarkan satu peserta dari pertama kali mendaftar sampai pembayara
|
||||
|--------|-------------|
|
||||
| Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer |
|
||||
| Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual |
|
||||
| Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, selfie, rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. |
|
||||
| Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, foto liveness (memegang kertas "SETRIP"), rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. |
|
||||
| Persetujuan T&C / Privasi | `User.acceptedTermsAndPrivacy` + `User.acceptedAt`, dicentang saat registrasi (link ke `/terms` & `/privacy`). |
|
||||
|
||||
## Menjalankan secara lokal
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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).
|
||||
|
||||
---
|
||||
|
||||
## Patch — KYC liveness photo rename (di luar fase social repositioning)
|
||||
|
||||
Mengubah foto liveness dari "selfie memegang KTP" (pola KYC standar) menjadi "memegang kertas tulisan SETRIP".
|
||||
|
||||
| Item | Status | File |
|
||||
|---|---|---|
|
||||
| Field `selfieKey` → `livenessKey` di `OrganizerVerification` | ✅ | [prisma/schema.prisma](prisma/schema.prisma), [prisma/migrations/20260508140000_rename_selfie_to_liveness/migration.sql](prisma/migrations/20260508140000_rename_selfie_to_liveness/migration.sql) |
|
||||
| Storage kind `selfie` → `liveness` (path `liveness/<id>.<ext>`) | ✅ | [lib/secure-storage.ts](lib/secure-storage.ts) |
|
||||
| Validasi + action + service + verify-form + review-card | ✅ | [features/organizer/schemas.ts](features/organizer/schemas.ts), [features/organizer/actions.ts](features/organizer/actions.ts), [server/services/organizer.service.ts](server/services/organizer.service.ts), [features/organizer/components/verify-form.tsx](features/organizer/components/verify-form.tsx), [features/organizer/components/review-card.tsx](features/organizer/components/review-card.tsx) |
|
||||
| API routes `/api/upload/kyc` & `/api/files/kyc/[id]/[kind]` | ✅ | [app/api/upload/kyc/route.ts](app/api/upload/kyc/route.ts), [app/api/files/kyc/[id]/[kind]/route.ts](app/api/files/kyc/%5Bid%5D/%5Bkind%5D/route.ts) |
|
||||
| Halaman verify, admin, seed, README, ARCHITECTURE | ✅ | [app/verify/page.tsx](app/verify/page.tsx), [app/admin/verifications/page.tsx](app/admin/verifications/page.tsx), [app/create-trip/page.tsx](app/create-trip/page.tsx), [prisma/seed.ts](prisma/seed.ts), [README.md](README.md), [ARCHITECTURE.md](ARCHITECTURE.md) |
|
||||
|
||||
**Trade-off keamanan yang sudah dikomunikasikan:** pola "selfie + KTP" lebih kuat (membuktikan KTP fisik di tangan pemilik). Pola "selfie + kertas SETRIP" lebih lemah dari sisi binding KTP↔orang, tapi mengurangi paparan KTP user dan masih mencegah replay dari platform lain. Risiko fraud naik sedikit — tetap dipilih atas request user.
|
||||
|
||||
**Catatan migrasi data lama:** kolom DB di-rename, tapi nilai-nilai key lama masih punya prefix `selfie/` (mis. `selfie/abc.jpg`). Setelah migration di-apply, validasi schema menolak prefix lama → user dengan pengajuan PENDING perlu re-upload foto liveness baru. Folder fisik `uploads/private/selfie/` tidak dipakai lagi, bisa dihapus manual setelah konfirmasi tidak ada data aktif yang merujuk.
|
||||
|
||||
**Tindakan manual:** jalankan `npx prisma migrate deploy` untuk apply `20260508140000_rename_selfie_to_liveness` (sekarang total 4 migration pending kalau belum pernah deploy).
|
||||
|
||||
---
|
||||
|
||||
## 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".
|
||||
@@ -60,7 +60,8 @@ export default async function AdminVerificationsPage({ searchParams }: PageProps
|
||||
Review Verifikasi Organizer
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-neutral-500">
|
||||
Periksa data KTP, selfie, dan rekening sebelum menyetujui.
|
||||
Periksa data KTP, foto liveness (memegang kertas SETRIP), dan rekening
|
||||
sebelum menyetujui.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function GET(_req: NextRequest, ctx: RouteCtx) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const key = kind === "ktp" ? verification.ktpImageKey : verification.selfieKey;
|
||||
const key = kind === "ktp" ? verification.ktpImageKey : verification.livenessKey;
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: "File belum diunggah" }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ export async function POST(req: NextRequest) {
|
||||
const file = form.get("file");
|
||||
|
||||
if (!isKycKind(kind)) {
|
||||
return NextResponse.json({ error: "kind harus 'ktp' atau 'selfie'" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "kind harus 'ktp' atau 'liveness'" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: "File wajib diisi" }, { status: 400 });
|
||||
|
||||
@@ -79,7 +79,7 @@ function VerificationBanner({
|
||||
<p className="mt-1 text-sm text-neutral-700">
|
||||
{isRejected
|
||||
? "Pengajuan sebelumnya ditolak. Untuk membuat trip berbayar, perbaiki data dan ajukan ulang."
|
||||
: "Untuk membuat trip berbayar, akun kamu perlu diverifikasi (KTP, selfie, & rekening). Trip gratis tidak butuh verifikasi."}
|
||||
: "Untuk membuat trip berbayar, akun kamu perlu diverifikasi (KTP, foto memegang kertas SETRIP, & rekening). Trip gratis tidak butuh verifikasi."}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
|
||||
@@ -22,6 +22,12 @@ export * from './enums';
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model UserProfile
|
||||
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||
* (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||
*/
|
||||
export type UserProfile = Prisma.UserProfileModel
|
||||
/**
|
||||
* Model Account
|
||||
* Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
||||
|
||||
@@ -46,6 +46,12 @@ export { Prisma }
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model UserProfile
|
||||
* Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||
* (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||
*/
|
||||
export type UserProfile = Prisma.UserProfileModel
|
||||
/**
|
||||
* Model Account
|
||||
* Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
||||
|
||||
@@ -148,6 +148,23 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
@@ -417,6 +434,23 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
* 🟢 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 = {
|
||||
PENDING: 'PENDING',
|
||||
APPROVED: 'APPROVED',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -385,6 +385,7 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
|
||||
|
||||
export const ModelName = {
|
||||
User: 'User',
|
||||
UserProfile: 'UserProfile',
|
||||
Account: 'Account',
|
||||
OrganizerVerification: 'OrganizerVerification',
|
||||
Trip: 'Trip',
|
||||
@@ -406,7 +407,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "user" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant"
|
||||
modelProps: "user" | "userProfile" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
@@ -484,6 +485,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
UserProfile: {
|
||||
payload: Prisma.$UserProfilePayload<ExtArgs>
|
||||
fields: Prisma.UserProfileFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.UserProfileFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.UserProfileFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.UserProfileFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.UserProfileFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.UserProfileFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.UserProfileCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.UserProfileCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.UserProfileCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.UserProfileDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.UserProfileUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.UserProfileDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.UserProfileUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.UserProfileUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.UserProfileUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserProfilePayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.UserProfileAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateUserProfile>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.UserProfileGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.UserProfileGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.UserProfileCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.UserProfileCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
Account: {
|
||||
payload: Prisma.$AccountPayload<ExtArgs>
|
||||
fields: Prisma.AccountFieldRefs
|
||||
@@ -983,6 +1058,21 @@ export const UserScalarFieldEnum = {
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
bio: 'bio',
|
||||
city: 'city',
|
||||
interests: 'interests',
|
||||
instagram: 'instagram',
|
||||
vibe: 'vibe',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type UserProfileScalarFieldEnum = (typeof UserProfileScalarFieldEnum)[keyof typeof UserProfileScalarFieldEnum]
|
||||
|
||||
|
||||
export const AccountScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
@@ -1010,7 +1100,7 @@ export const OrganizerVerificationScalarFieldEnum = {
|
||||
birthDate: 'birthDate',
|
||||
address: 'address',
|
||||
ktpImageKey: 'ktpImageKey',
|
||||
selfieKey: 'selfieKey',
|
||||
livenessKey: 'livenessKey',
|
||||
bankName: 'bankName',
|
||||
bankAccountNumber: 'bankAccountNumber',
|
||||
bankAccountName: 'bankAccountName',
|
||||
@@ -1041,6 +1131,7 @@ export const TripScalarFieldEnum = {
|
||||
endDate: 'endDate',
|
||||
maxParticipants: 'maxParticipants',
|
||||
price: 'price',
|
||||
vibe: 'vibe',
|
||||
status: 'status',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
@@ -1152,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'
|
||||
*/
|
||||
@@ -1346,6 +1451,7 @@ export type PrismaClientOptions = ({
|
||||
}
|
||||
export type GlobalOmitConfig = {
|
||||
user?: Prisma.UserOmit
|
||||
userProfile?: Prisma.UserProfileOmit
|
||||
account?: Prisma.AccountOmit
|
||||
organizerVerification?: Prisma.OrganizerVerificationOmit
|
||||
trip?: Prisma.TripOmit
|
||||
|
||||
@@ -52,6 +52,7 @@ export const AnyNull = runtime.AnyNull
|
||||
|
||||
export const ModelName = {
|
||||
User: 'User',
|
||||
UserProfile: 'UserProfile',
|
||||
Account: 'Account',
|
||||
OrganizerVerification: 'OrganizerVerification',
|
||||
Trip: 'Trip',
|
||||
@@ -92,6 +93,21 @@ export const UserScalarFieldEnum = {
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
bio: 'bio',
|
||||
city: 'city',
|
||||
interests: 'interests',
|
||||
instagram: 'instagram',
|
||||
vibe: 'vibe',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type UserProfileScalarFieldEnum = (typeof UserProfileScalarFieldEnum)[keyof typeof UserProfileScalarFieldEnum]
|
||||
|
||||
|
||||
export const AccountScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
@@ -119,7 +135,7 @@ export const OrganizerVerificationScalarFieldEnum = {
|
||||
birthDate: 'birthDate',
|
||||
address: 'address',
|
||||
ktpImageKey: 'ktpImageKey',
|
||||
selfieKey: 'selfieKey',
|
||||
livenessKey: 'livenessKey',
|
||||
bankName: 'bankName',
|
||||
bankAccountNumber: 'bankAccountNumber',
|
||||
bankAccountName: 'bankAccountName',
|
||||
@@ -150,6 +166,7 @@ export const TripScalarFieldEnum = {
|
||||
endDate: 'endDate',
|
||||
maxParticipants: 'maxParticipants',
|
||||
price: 'price',
|
||||
vibe: 'vibe',
|
||||
status: 'status',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/User'
|
||||
export type * from './models/UserProfile'
|
||||
export type * from './models/Account'
|
||||
export type * from './models/OrganizerVerification'
|
||||
export type * from './models/Trip'
|
||||
|
||||
@@ -33,7 +33,7 @@ export type OrganizerVerificationMinAggregateOutputType = {
|
||||
birthDate: Date | null
|
||||
address: string | null
|
||||
ktpImageKey: string | null
|
||||
selfieKey: string | null
|
||||
livenessKey: string | null
|
||||
bankName: string | null
|
||||
bankAccountNumber: string | null
|
||||
bankAccountName: string | null
|
||||
@@ -55,7 +55,7 @@ export type OrganizerVerificationMaxAggregateOutputType = {
|
||||
birthDate: Date | null
|
||||
address: string | null
|
||||
ktpImageKey: string | null
|
||||
selfieKey: string | null
|
||||
livenessKey: string | null
|
||||
bankName: string | null
|
||||
bankAccountNumber: string | null
|
||||
bankAccountName: string | null
|
||||
@@ -77,7 +77,7 @@ export type OrganizerVerificationCountAggregateOutputType = {
|
||||
birthDate: number
|
||||
address: number
|
||||
ktpImageKey: number
|
||||
selfieKey: number
|
||||
livenessKey: number
|
||||
bankName: number
|
||||
bankAccountNumber: number
|
||||
bankAccountName: number
|
||||
@@ -101,7 +101,7 @@ export type OrganizerVerificationMinAggregateInputType = {
|
||||
birthDate?: true
|
||||
address?: true
|
||||
ktpImageKey?: true
|
||||
selfieKey?: true
|
||||
livenessKey?: true
|
||||
bankName?: true
|
||||
bankAccountNumber?: true
|
||||
bankAccountName?: true
|
||||
@@ -123,7 +123,7 @@ export type OrganizerVerificationMaxAggregateInputType = {
|
||||
birthDate?: true
|
||||
address?: true
|
||||
ktpImageKey?: true
|
||||
selfieKey?: true
|
||||
livenessKey?: true
|
||||
bankName?: true
|
||||
bankAccountNumber?: true
|
||||
bankAccountName?: true
|
||||
@@ -145,7 +145,7 @@ export type OrganizerVerificationCountAggregateInputType = {
|
||||
birthDate?: true
|
||||
address?: true
|
||||
ktpImageKey?: true
|
||||
selfieKey?: true
|
||||
livenessKey?: true
|
||||
bankName?: true
|
||||
bankAccountNumber?: true
|
||||
bankAccountName?: true
|
||||
@@ -240,7 +240,7 @@ export type OrganizerVerificationGroupByOutputType = {
|
||||
birthDate: Date
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -283,7 +283,7 @@ export type OrganizerVerificationWhereInput = {
|
||||
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
|
||||
address?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
@@ -307,7 +307,7 @@ export type OrganizerVerificationOrderByWithRelationInput = {
|
||||
birthDate?: Prisma.SortOrder
|
||||
address?: Prisma.SortOrder
|
||||
ktpImageKey?: Prisma.SortOrder
|
||||
selfieKey?: Prisma.SortOrder
|
||||
livenessKey?: Prisma.SortOrder
|
||||
bankName?: Prisma.SortOrder
|
||||
bankAccountNumber?: Prisma.SortOrder
|
||||
bankAccountName?: Prisma.SortOrder
|
||||
@@ -334,7 +334,7 @@ export type OrganizerVerificationWhereUniqueInput = Prisma.AtLeast<{
|
||||
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
|
||||
address?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
@@ -358,7 +358,7 @@ export type OrganizerVerificationOrderByWithAggregationInput = {
|
||||
birthDate?: Prisma.SortOrder
|
||||
address?: Prisma.SortOrder
|
||||
ktpImageKey?: Prisma.SortOrder
|
||||
selfieKey?: Prisma.SortOrder
|
||||
livenessKey?: Prisma.SortOrder
|
||||
bankName?: Prisma.SortOrder
|
||||
bankAccountNumber?: Prisma.SortOrder
|
||||
bankAccountName?: Prisma.SortOrder
|
||||
@@ -386,7 +386,7 @@ export type OrganizerVerificationScalarWhereWithAggregatesInput = {
|
||||
birthDate?: Prisma.DateTimeWithAggregatesFilter<"OrganizerVerification"> | Date | string
|
||||
address?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
ktpImageKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
selfieKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
livenessKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
bankName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
bankAccountNumber?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
bankAccountName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
|
||||
@@ -407,7 +407,7 @@ export type OrganizerVerificationCreateInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -430,7 +430,7 @@ export type OrganizerVerificationUncheckedCreateInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -451,7 +451,7 @@ export type OrganizerVerificationUpdateInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -474,7 +474,7 @@ export type OrganizerVerificationUncheckedUpdateInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -496,7 +496,7 @@ export type OrganizerVerificationCreateManyInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -517,7 +517,7 @@ export type OrganizerVerificationUpdateManyMutationInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -538,7 +538,7 @@ export type OrganizerVerificationUncheckedUpdateManyInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -575,7 +575,7 @@ export type OrganizerVerificationCountOrderByAggregateInput = {
|
||||
birthDate?: Prisma.SortOrder
|
||||
address?: Prisma.SortOrder
|
||||
ktpImageKey?: Prisma.SortOrder
|
||||
selfieKey?: Prisma.SortOrder
|
||||
livenessKey?: Prisma.SortOrder
|
||||
bankName?: Prisma.SortOrder
|
||||
bankAccountNumber?: Prisma.SortOrder
|
||||
bankAccountName?: Prisma.SortOrder
|
||||
@@ -597,7 +597,7 @@ export type OrganizerVerificationMaxOrderByAggregateInput = {
|
||||
birthDate?: Prisma.SortOrder
|
||||
address?: Prisma.SortOrder
|
||||
ktpImageKey?: Prisma.SortOrder
|
||||
selfieKey?: Prisma.SortOrder
|
||||
livenessKey?: Prisma.SortOrder
|
||||
bankName?: Prisma.SortOrder
|
||||
bankAccountNumber?: Prisma.SortOrder
|
||||
bankAccountName?: Prisma.SortOrder
|
||||
@@ -619,7 +619,7 @@ export type OrganizerVerificationMinOrderByAggregateInput = {
|
||||
birthDate?: Prisma.SortOrder
|
||||
address?: Prisma.SortOrder
|
||||
ktpImageKey?: Prisma.SortOrder
|
||||
selfieKey?: Prisma.SortOrder
|
||||
livenessKey?: Prisma.SortOrder
|
||||
bankName?: Prisma.SortOrder
|
||||
bankAccountNumber?: Prisma.SortOrder
|
||||
bankAccountName?: Prisma.SortOrder
|
||||
@@ -718,7 +718,7 @@ export type OrganizerVerificationCreateWithoutUserInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -739,7 +739,7 @@ export type OrganizerVerificationUncheckedCreateWithoutUserInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -765,7 +765,7 @@ export type OrganizerVerificationCreateWithoutReviewedByInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -787,7 +787,7 @@ export type OrganizerVerificationUncheckedCreateWithoutReviewedByInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -828,7 +828,7 @@ export type OrganizerVerificationUpdateWithoutUserInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -849,7 +849,7 @@ export type OrganizerVerificationUncheckedUpdateWithoutUserInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -890,7 +890,7 @@ export type OrganizerVerificationScalarWhereInput = {
|
||||
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
|
||||
address?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
|
||||
@@ -912,7 +912,7 @@ export type OrganizerVerificationCreateManyReviewedByInput = {
|
||||
birthDate: Date | string
|
||||
address: string
|
||||
ktpImageKey: string
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -932,7 +932,7 @@ export type OrganizerVerificationUpdateWithoutReviewedByInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -954,7 +954,7 @@ export type OrganizerVerificationUncheckedUpdateWithoutReviewedByInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -975,7 +975,7 @@ export type OrganizerVerificationUncheckedUpdateManyWithoutReviewedByInput = {
|
||||
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
address?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -998,7 +998,7 @@ export type OrganizerVerificationSelect<ExtArgs extends runtime.Types.Extensions
|
||||
birthDate?: boolean
|
||||
address?: boolean
|
||||
ktpImageKey?: boolean
|
||||
selfieKey?: boolean
|
||||
livenessKey?: boolean
|
||||
bankName?: boolean
|
||||
bankAccountNumber?: boolean
|
||||
bankAccountName?: boolean
|
||||
@@ -1022,7 +1022,7 @@ export type OrganizerVerificationSelectCreateManyAndReturn<ExtArgs extends runti
|
||||
birthDate?: boolean
|
||||
address?: boolean
|
||||
ktpImageKey?: boolean
|
||||
selfieKey?: boolean
|
||||
livenessKey?: boolean
|
||||
bankName?: boolean
|
||||
bankAccountNumber?: boolean
|
||||
bankAccountName?: boolean
|
||||
@@ -1046,7 +1046,7 @@ export type OrganizerVerificationSelectUpdateManyAndReturn<ExtArgs extends runti
|
||||
birthDate?: boolean
|
||||
address?: boolean
|
||||
ktpImageKey?: boolean
|
||||
selfieKey?: boolean
|
||||
livenessKey?: boolean
|
||||
bankName?: boolean
|
||||
bankAccountNumber?: boolean
|
||||
bankAccountName?: boolean
|
||||
@@ -1070,7 +1070,7 @@ export type OrganizerVerificationSelectScalar = {
|
||||
birthDate?: boolean
|
||||
address?: boolean
|
||||
ktpImageKey?: boolean
|
||||
selfieKey?: boolean
|
||||
livenessKey?: boolean
|
||||
bankName?: boolean
|
||||
bankAccountNumber?: boolean
|
||||
bankAccountName?: boolean
|
||||
@@ -1083,7 +1083,7 @@ export type OrganizerVerificationSelectScalar = {
|
||||
updatedAt?: boolean
|
||||
}
|
||||
|
||||
export type OrganizerVerificationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "fullName" | "nikEncrypted" | "nikHash" | "birthDate" | "address" | "ktpImageKey" | "selfieKey" | "bankName" | "bankAccountNumber" | "bankAccountName" | "status" | "rejectionReason" | "reviewedAt" | "reviewedById" | "verifiedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["organizerVerification"]>
|
||||
export type OrganizerVerificationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "fullName" | "nikEncrypted" | "nikHash" | "birthDate" | "address" | "ktpImageKey" | "livenessKey" | "bankName" | "bankAccountNumber" | "bankAccountName" | "status" | "rejectionReason" | "reviewedAt" | "reviewedById" | "verifiedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["organizerVerification"]>
|
||||
export type OrganizerVerificationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
reviewedBy?: boolean | Prisma.OrganizerVerification$reviewedByArgs<ExtArgs>
|
||||
@@ -1125,9 +1125,10 @@ export type $OrganizerVerificationPayload<ExtArgs extends runtime.Types.Extensio
|
||||
*/
|
||||
ktpImageKey: string
|
||||
/**
|
||||
* Storage key selfie memegang KTP.
|
||||
* Storage key foto liveness — user memegang kertas bertuliskan "SETRIP".
|
||||
* (Sebelumnya: selfie memegang KTP. Diganti supaya user tidak perlu memajang KTP dua kali.)
|
||||
*/
|
||||
selfieKey: string
|
||||
livenessKey: string
|
||||
bankName: string
|
||||
bankAccountNumber: string
|
||||
bankAccountName: string
|
||||
@@ -1571,7 +1572,7 @@ export interface OrganizerVerificationFieldRefs {
|
||||
readonly birthDate: Prisma.FieldRef<"OrganizerVerification", 'DateTime'>
|
||||
readonly address: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly ktpImageKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly selfieKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly livenessKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly bankName: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly bankAccountNumber: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
readonly bankAccountName: Prisma.FieldRef<"OrganizerVerification", 'String'>
|
||||
|
||||
@@ -51,6 +51,7 @@ export type TripMinAggregateOutputType = {
|
||||
endDate: Date | null
|
||||
maxParticipants: number | null
|
||||
price: number | null
|
||||
vibe: $Enums.Vibe | null
|
||||
status: $Enums.TripStatus | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
@@ -72,6 +73,7 @@ export type TripMaxAggregateOutputType = {
|
||||
endDate: Date | null
|
||||
maxParticipants: number | null
|
||||
price: number | null
|
||||
vibe: $Enums.Vibe | null
|
||||
status: $Enums.TripStatus | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
@@ -93,6 +95,7 @@ export type TripCountAggregateOutputType = {
|
||||
endDate: number
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe: number
|
||||
status: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
@@ -126,6 +129,7 @@ export type TripMinAggregateInputType = {
|
||||
endDate?: true
|
||||
maxParticipants?: true
|
||||
price?: true
|
||||
vibe?: true
|
||||
status?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@@ -147,6 +151,7 @@ export type TripMaxAggregateInputType = {
|
||||
endDate?: true
|
||||
maxParticipants?: true
|
||||
price?: true
|
||||
vibe?: true
|
||||
status?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@@ -168,6 +173,7 @@ export type TripCountAggregateInputType = {
|
||||
endDate?: true
|
||||
maxParticipants?: true
|
||||
price?: true
|
||||
vibe?: true
|
||||
status?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@@ -276,6 +282,7 @@ export type TripGroupByOutputType = {
|
||||
endDate: Date | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe: $Enums.Vibe | null
|
||||
status: $Enums.TripStatus
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@@ -320,6 +327,7 @@ export type TripWhereInput = {
|
||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||
price?: Prisma.IntFilter<"Trip"> | number
|
||||
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
@@ -345,6 +353,7 @@ export type TripOrderByWithRelationInput = {
|
||||
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
maxParticipants?: Prisma.SortOrder
|
||||
price?: Prisma.SortOrder
|
||||
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
@@ -373,6 +382,7 @@ export type TripWhereUniqueInput = Prisma.AtLeast<{
|
||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||
price?: Prisma.IntFilter<"Trip"> | number
|
||||
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
@@ -398,6 +408,7 @@ export type TripOrderByWithAggregationInput = {
|
||||
endDate?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
maxParticipants?: Prisma.SortOrder
|
||||
price?: Prisma.SortOrder
|
||||
vibe?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
@@ -427,6 +438,7 @@ export type TripScalarWhereWithAggregatesInput = {
|
||||
endDate?: Prisma.DateTimeNullableWithAggregatesFilter<"Trip"> | Date | string | null
|
||||
maxParticipants?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
||||
price?: Prisma.IntWithAggregatesFilter<"Trip"> | number
|
||||
vibe?: Prisma.EnumVibeNullableWithAggregatesFilter<"Trip"> | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusWithAggregatesFilter<"Trip"> | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Trip"> | Date | string
|
||||
@@ -448,6 +460,7 @@ export type TripCreateInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -472,6 +485,7 @@ export type TripUncheckedCreateInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -496,6 +510,7 @@ export type TripUpdateInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -520,6 +535,7 @@ export type TripUncheckedUpdateInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -544,6 +560,7 @@ export type TripCreateManyInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -565,6 +582,7 @@ export type TripUpdateManyMutationInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -585,6 +603,7 @@ export type TripUncheckedUpdateManyInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -616,6 +635,7 @@ export type TripCountOrderByAggregateInput = {
|
||||
endDate?: Prisma.SortOrder
|
||||
maxParticipants?: Prisma.SortOrder
|
||||
price?: Prisma.SortOrder
|
||||
vibe?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
@@ -642,6 +662,7 @@ export type TripMaxOrderByAggregateInput = {
|
||||
endDate?: Prisma.SortOrder
|
||||
maxParticipants?: Prisma.SortOrder
|
||||
price?: Prisma.SortOrder
|
||||
vibe?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
@@ -663,6 +684,7 @@ export type TripMinOrderByAggregateInput = {
|
||||
endDate?: Prisma.SortOrder
|
||||
maxParticipants?: Prisma.SortOrder
|
||||
price?: Prisma.SortOrder
|
||||
vibe?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
@@ -794,6 +816,7 @@ export type TripCreateWithoutOrganizerInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -817,6 +840,7 @@ export type TripUncheckedCreateWithoutOrganizerInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -869,6 +893,7 @@ export type TripScalarWhereInput = {
|
||||
endDate?: Prisma.DateTimeNullableFilter<"Trip"> | Date | string | null
|
||||
maxParticipants?: Prisma.IntFilter<"Trip"> | number
|
||||
price?: Prisma.IntFilter<"Trip"> | number
|
||||
vibe?: Prisma.EnumVibeNullableFilter<"Trip"> | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFilter<"Trip"> | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Trip"> | Date | string
|
||||
@@ -890,6 +915,7 @@ export type TripCreateWithoutReviewsInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -913,6 +939,7 @@ export type TripUncheckedCreateWithoutReviewsInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -952,6 +979,7 @@ export type TripUpdateWithoutReviewsInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -975,6 +1003,7 @@ export type TripUncheckedUpdateWithoutReviewsInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -998,6 +1027,7 @@ export type TripCreateWithoutImagesInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -1021,6 +1051,7 @@ export type TripUncheckedCreateWithoutImagesInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -1060,6 +1091,7 @@ export type TripUpdateWithoutImagesInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1083,6 +1115,7 @@ export type TripUncheckedUpdateWithoutImagesInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1106,6 +1139,7 @@ export type TripCreateWithoutParticipantsInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -1129,6 +1163,7 @@ export type TripUncheckedCreateWithoutParticipantsInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -1168,6 +1203,7 @@ export type TripUpdateWithoutParticipantsInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1191,6 +1227,7 @@ export type TripUncheckedUpdateWithoutParticipantsInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1214,6 +1251,7 @@ export type TripCreateManyOrganizerInput = {
|
||||
endDate?: Date | string | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
vibe?: $Enums.Vibe | null
|
||||
status?: $Enums.TripStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@@ -1234,6 +1272,7 @@ export type TripUpdateWithoutOrganizerInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1257,6 +1296,7 @@ export type TripUncheckedUpdateWithoutOrganizerInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@@ -1280,6 +1320,7 @@ export type TripUncheckedUpdateManyWithoutOrganizerInput = {
|
||||
endDate?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
maxParticipants?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
price?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
vibe?: Prisma.NullableEnumVibeFieldUpdateOperationsInput | $Enums.Vibe | null
|
||||
status?: Prisma.EnumTripStatusFieldUpdateOperationsInput | $Enums.TripStatus
|
||||
createdAt?: 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
|
||||
maxParticipants?: boolean
|
||||
price?: boolean
|
||||
vibe?: boolean
|
||||
status?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@@ -1375,6 +1417,7 @@ export type TripSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
||||
endDate?: boolean
|
||||
maxParticipants?: boolean
|
||||
price?: boolean
|
||||
vibe?: boolean
|
||||
status?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@@ -1397,6 +1440,7 @@ export type TripSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
||||
endDate?: boolean
|
||||
maxParticipants?: boolean
|
||||
price?: boolean
|
||||
vibe?: boolean
|
||||
status?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@@ -1419,13 +1463,14 @@ export type TripSelectScalar = {
|
||||
endDate?: boolean
|
||||
maxParticipants?: boolean
|
||||
price?: boolean
|
||||
vibe?: boolean
|
||||
status?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: 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> = {
|
||||
organizer?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
participants?: boolean | Prisma.Trip$participantsArgs<ExtArgs>
|
||||
@@ -1481,6 +1526,10 @@ export type $TripPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
endDate: Date | null
|
||||
maxParticipants: number
|
||||
price: number
|
||||
/**
|
||||
* Ritme/energi trip — dipakai untuk matching dengan vibe user.
|
||||
*/
|
||||
vibe: $Enums.Vibe | null
|
||||
status: $Enums.TripStatus
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@@ -1926,6 +1975,7 @@ export interface TripFieldRefs {
|
||||
readonly endDate: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||
readonly maxParticipants: Prisma.FieldRef<"Trip", 'Int'>
|
||||
readonly price: Prisma.FieldRef<"Trip", 'Int'>
|
||||
readonly vibe: Prisma.FieldRef<"Trip", 'Vibe'>
|
||||
readonly status: Prisma.FieldRef<"Trip", 'TripStatus'>
|
||||
readonly createdAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||
readonly updatedAt: Prisma.FieldRef<"Trip", 'DateTime'>
|
||||
|
||||
@@ -228,6 +228,7 @@ export type UserWhereInput = {
|
||||
tripReviews?: Prisma.TripReviewListRelationFilter
|
||||
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
||||
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
||||
}
|
||||
|
||||
export type UserOrderByWithRelationInput = {
|
||||
@@ -247,6 +248,7 @@ export type UserOrderByWithRelationInput = {
|
||||
tripReviews?: Prisma.TripReviewOrderByRelationAggregateInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationOrderByWithRelationInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationOrderByRelationAggregateInput
|
||||
profile?: Prisma.UserProfileOrderByWithRelationInput
|
||||
}
|
||||
|
||||
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -269,6 +271,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||
tripReviews?: Prisma.TripReviewListRelationFilter
|
||||
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
||||
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
||||
}, "id" | "email">
|
||||
|
||||
export type UserOrderByWithAggregationInput = {
|
||||
@@ -320,6 +323,7 @@ export type UserCreateInput = {
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateInput = {
|
||||
@@ -339,6 +343,7 @@ export type UserUncheckedCreateInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUpdateInput = {
|
||||
@@ -358,6 +363,7 @@ export type UserUpdateInput = {
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateInput = {
|
||||
@@ -377,6 +383,7 @@ export type UserUncheckedUpdateInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateManyInput = {
|
||||
@@ -487,6 +494,20 @@ export type DateTimeFieldUpdateOperationsInput = {
|
||||
set?: Date | string
|
||||
}
|
||||
|
||||
export type UserCreateNestedOneWithoutProfileInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
}
|
||||
|
||||
export type UserUpdateOneRequiredWithoutProfileNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
|
||||
upsert?: Prisma.UserUpsertWithoutProfileInput
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutProfileInput, Prisma.UserUpdateWithoutProfileInput>, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
}
|
||||
|
||||
export type UserCreateNestedOneWithoutAccountsInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutAccountsInput
|
||||
@@ -573,6 +594,98 @@ export type UserUpdateOneRequiredWithoutParticipationsNestedInput = {
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutParticipationsInput, Prisma.UserUpdateWithoutParticipationsInput>, Prisma.UserUncheckedUpdateWithoutParticipationsInput>
|
||||
}
|
||||
|
||||
export type UserCreateWithoutProfileInput = {
|
||||
id?: string
|
||||
name: string
|
||||
email: string
|
||||
password?: string | null
|
||||
image?: string | null
|
||||
emailVerified?: Date | string | null
|
||||
acceptedTermsAndPrivacy?: boolean
|
||||
acceptedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutProfileInput = {
|
||||
id?: string
|
||||
name: string
|
||||
email: string
|
||||
password?: string | null
|
||||
image?: string | null
|
||||
emailVerified?: Date | string | null
|
||||
acceptedTermsAndPrivacy?: boolean
|
||||
acceptedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutProfileInput = {
|
||||
where: Prisma.UserWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
}
|
||||
|
||||
export type UserUpsertWithoutProfileInput = {
|
||||
update: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
where?: Prisma.UserWhereInput
|
||||
}
|
||||
|
||||
export type UserUpdateToOneWithWhereWithoutProfileInput = {
|
||||
where?: Prisma.UserWhereInput
|
||||
data: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
}
|
||||
|
||||
export type UserUpdateWithoutProfileInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutProfileInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateWithoutAccountsInput = {
|
||||
id?: string
|
||||
name: string
|
||||
@@ -589,6 +702,7 @@ export type UserCreateWithoutAccountsInput = {
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutAccountsInput = {
|
||||
@@ -607,6 +721,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutAccountsInput = {
|
||||
@@ -641,6 +756,7 @@ export type UserUpdateWithoutAccountsInput = {
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutAccountsInput = {
|
||||
@@ -659,6 +775,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateWithoutOrganizerVerificationInput = {
|
||||
@@ -677,6 +794,7 @@ export type UserCreateWithoutOrganizerVerificationInput = {
|
||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutOrganizerVerificationInput = {
|
||||
@@ -695,6 +813,7 @@ export type UserUncheckedCreateWithoutOrganizerVerificationInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutOrganizerVerificationInput = {
|
||||
@@ -718,6 +837,7 @@ export type UserCreateWithoutReviewedVerificationsInput = {
|
||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutReviewedVerificationsInput = {
|
||||
@@ -736,6 +856,7 @@ export type UserUncheckedCreateWithoutReviewedVerificationsInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutReviewedVerificationsInput = {
|
||||
@@ -770,6 +891,7 @@ export type UserUpdateWithoutOrganizerVerificationInput = {
|
||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutOrganizerVerificationInput = {
|
||||
@@ -788,6 +910,7 @@ export type UserUncheckedUpdateWithoutOrganizerVerificationInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUpsertWithoutReviewedVerificationsInput = {
|
||||
@@ -817,6 +940,7 @@ export type UserUpdateWithoutReviewedVerificationsInput = {
|
||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutReviewedVerificationsInput = {
|
||||
@@ -835,6 +959,7 @@ export type UserUncheckedUpdateWithoutReviewedVerificationsInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateWithoutTripsInput = {
|
||||
@@ -853,6 +978,7 @@ export type UserCreateWithoutTripsInput = {
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutTripsInput = {
|
||||
@@ -871,6 +997,7 @@ export type UserUncheckedCreateWithoutTripsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutTripsInput = {
|
||||
@@ -905,6 +1032,7 @@ export type UserUpdateWithoutTripsInput = {
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutTripsInput = {
|
||||
@@ -923,6 +1051,7 @@ export type UserUncheckedUpdateWithoutTripsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateWithoutTripReviewsInput = {
|
||||
@@ -941,6 +1070,7 @@ export type UserCreateWithoutTripReviewsInput = {
|
||||
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutTripReviewsInput = {
|
||||
@@ -959,6 +1089,7 @@ export type UserUncheckedCreateWithoutTripReviewsInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutTripReviewsInput = {
|
||||
@@ -993,6 +1124,7 @@ export type UserUpdateWithoutTripReviewsInput = {
|
||||
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutTripReviewsInput = {
|
||||
@@ -1011,6 +1143,7 @@ export type UserUncheckedUpdateWithoutTripReviewsInput = {
|
||||
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserCreateWithoutParticipationsInput = {
|
||||
@@ -1029,6 +1162,7 @@ export type UserCreateWithoutParticipationsInput = {
|
||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutParticipationsInput = {
|
||||
@@ -1047,6 +1181,7 @@ export type UserUncheckedCreateWithoutParticipationsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutParticipationsInput = {
|
||||
@@ -1081,6 +1216,7 @@ export type UserUpdateWithoutParticipationsInput = {
|
||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutParticipationsInput = {
|
||||
@@ -1099,6 +1235,7 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
|
||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
}
|
||||
|
||||
|
||||
@@ -1185,6 +1322,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
||||
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
|
||||
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
||||
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["user"]>
|
||||
|
||||
@@ -1235,6 +1373,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
|
||||
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
||||
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||
@@ -1249,6 +1388,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
tripReviews: Prisma.$TripReviewPayload<ExtArgs>[]
|
||||
organizerVerification: Prisma.$OrganizerVerificationPayload<ExtArgs> | null
|
||||
reviewedVerifications: Prisma.$OrganizerVerificationPayload<ExtArgs>[]
|
||||
profile: Prisma.$UserProfilePayload<ExtArgs> | null
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -1673,6 +1813,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
|
||||
tripReviews<T extends Prisma.User$tripReviewsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripReviewsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripReviewPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
organizerVerification<T extends Prisma.User$organizerVerificationArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$organizerVerificationArgs<ExtArgs>>): Prisma.Prisma__OrganizerVerificationClient<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
reviewedVerifications<T extends Prisma.User$reviewedVerificationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedVerificationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__UserProfileClient<runtime.Types.Result.GetResult<Prisma.$UserProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -2243,6 +2384,25 @@ export type User$reviewedVerificationsArgs<ExtArgs extends runtime.Types.Extensi
|
||||
distinct?: Prisma.OrganizerVerificationScalarFieldEnum | Prisma.OrganizerVerificationScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* User.profile
|
||||
*/
|
||||
export type User$profileArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the UserProfile
|
||||
*/
|
||||
select?: Prisma.UserProfileSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the UserProfile
|
||||
*/
|
||||
omit?: Prisma.UserProfileOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.UserProfileInclude<ExtArgs> | null
|
||||
where?: Prisma.UserProfileWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* User without action
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { SessionProvider } from "@/components/providers/session-provider";
|
||||
import { Navbar } from "@/components/shared/navbar";
|
||||
import { ProfileNudgeBanner } from "@/components/shared/profile-nudge-banner";
|
||||
import { siteConfig, siteUrl } from "@/lib/site";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -80,6 +81,7 @@ export default function RootLayout({
|
||||
<body className="flex min-h-full flex-col bg-neutral-50">
|
||||
<SessionProvider>
|
||||
<Navbar />
|
||||
<ProfileNudgeBanner />
|
||||
<main className="flex-1">{children}</main>
|
||||
</SessionProvider>
|
||||
</body>
|
||||
|
||||
+41
-9
@@ -1,11 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.service";
|
||||
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||
import { TripCard } from "@/features/trip/components/trip-card";
|
||||
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
||||
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 = {
|
||||
title: "Cari Teman Trip & Aktivitas — Pergi Bareng, Bukan Sendiri",
|
||||
description: `${siteConfig.slogan} ${siteConfig.description}`,
|
||||
@@ -18,7 +32,14 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
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 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 budgetTrips = trips
|
||||
.filter((t) => !shownIds.has(t.id) && t.price <= 300000)
|
||||
// Section sosial: trip yang paling ramai joiner-nya (social proof, bukan price proof).
|
||||
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);
|
||||
|
||||
const orgJsonLd = {
|
||||
@@ -191,6 +214,7 @@ export default async function HomePage() {
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
@@ -204,6 +228,8 @@ export default async function HomePage() {
|
||||
isVerifiedOrganizer={
|
||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||
}
|
||||
participants={mapParticipants(trip)}
|
||||
viewerInterests={viewerInterests}
|
||||
priority={i === 0}
|
||||
/>
|
||||
))}
|
||||
@@ -261,6 +287,7 @@ export default async function HomePage() {
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
@@ -274,35 +301,38 @@ export default async function HomePage() {
|
||||
isVerifiedOrganizer={
|
||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||
}
|
||||
participants={mapParticipants(trip)}
|
||||
viewerInterests={viewerInterests}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Budget Friendly */}
|
||||
{budgetTrips.length > 0 && (
|
||||
{/* Lagi Ramai — social proof, bukan price proof */}
|
||||
{buzzingTrips.length > 0 && (
|
||||
<section>
|
||||
<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>
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
|
||||
Budget Friendly
|
||||
Lagi Ramai
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{budgetTrips.map((trip) => (
|
||||
{buzzingTrips.map((trip) => (
|
||||
<TripCard
|
||||
key={trip.id}
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
@@ -316,6 +346,8 @@ export default async function HomePage() {
|
||||
isVerifiedOrganizer={
|
||||
trip.organizer.organizerVerification?.status === "APPROVED"
|
||||
}
|
||||
participants={mapParticipants(trip)}
|
||||
viewerInterests={viewerInterests}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
+24
-1
@@ -7,6 +7,7 @@ import { authOptions } from "@/lib/auth";
|
||||
import { profileService } from "@/server/services/profile.service";
|
||||
import { TripCard } from "@/features/trip/components/trip-card";
|
||||
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||
import { ProfileEditor } from "@/features/profile/components/profile-editor";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Profil Saya",
|
||||
@@ -19,7 +20,10 @@ export default async function ProfilePage() {
|
||||
redirect("/login?callbackUrl=/profile");
|
||||
}
|
||||
|
||||
const data = await profileService.getProfileDashboard(session.user.id);
|
||||
const [data, ownProfile] = await Promise.all([
|
||||
profileService.getProfileDashboard(session.user.id),
|
||||
profileService.getOwnProfile(session.user.id),
|
||||
]);
|
||||
const {
|
||||
user,
|
||||
isVerifiedOrganizer,
|
||||
@@ -80,6 +84,24 @@ export default async function ProfilePage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Profil sosial publik */}
|
||||
<div className="mb-6">
|
||||
<ProfileEditor
|
||||
userId={user.id}
|
||||
initial={
|
||||
ownProfile
|
||||
? {
|
||||
bio: ownProfile.bio,
|
||||
city: ownProfile.city,
|
||||
interests: ownProfile.interests,
|
||||
instagram: ownProfile.instagram,
|
||||
vibe: ownProfile.vibe,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trip selesai — akses ulasan (trip ini tidak muncul di Open Trip) */}
|
||||
{reviewable.length > 0 && (
|
||||
<section className="mb-8 rounded-2xl border border-amber-200 bg-amber-50/60 p-4 sm:p-5">
|
||||
@@ -148,6 +170,7 @@ export default async function ProfilePage() {
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
|
||||
+84
-20
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.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 { ImageGallery } from "@/features/trip/components/image-gallery";
|
||||
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||
import { categoryMeta } from "@/lib/activity-category";
|
||||
import { vibeMeta } from "@/lib/vibe";
|
||||
import {
|
||||
isPastTripLastDayForReview,
|
||||
isTripDepartureDayPast,
|
||||
@@ -127,6 +130,8 @@ export default async function TripDetailPage({
|
||||
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
||||
);
|
||||
|
||||
const catMeta = categoryMeta(trip.category);
|
||||
|
||||
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
||||
const eventStatus =
|
||||
trip.status === "OPEN"
|
||||
@@ -240,8 +245,21 @@ export default async function TripDetailPage({
|
||||
<h1 className="text-lg font-bold text-neutral-800 sm:text-xl">
|
||||
{trip.title}
|
||||
</h1>
|
||||
<p className="mt-0.5 flex items-center gap-1.5 text-sm text-neutral-500">
|
||||
🏔️ {trip.destination}
|
||||
<p className="mt-0.5 flex flex-wrap items-center gap-1.5 text-sm text-neutral-500">
|
||||
<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>
|
||||
</div>
|
||||
<span
|
||||
@@ -301,9 +319,12 @@ export default async function TripDetailPage({
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-medium text-neutral-400 sm:text-xs">Organizer</p>
|
||||
<p className="truncate text-xs font-semibold text-neutral-800 sm:text-sm">
|
||||
<Link
|
||||
href={`/u/${trip.organizer.id}`}
|
||||
className="truncate text-xs font-semibold text-neutral-800 hover:text-primary-700 sm:text-sm"
|
||||
>
|
||||
{trip.organizer.name}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,9 +466,12 @@ export default async function TripDetailPage({
|
||||
|
||||
{/* Peserta yang sudah disetujui organizer (publik) */}
|
||||
<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})
|
||||
</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 ? (
|
||||
<p className="text-xs text-neutral-400 sm:text-sm">
|
||||
Belum ada peserta yang dikonfirmasi.{" "}
|
||||
@@ -456,21 +480,61 @@ export default async function TripDetailPage({
|
||||
: "Jadilah yang pertama mendaftar! 🎒"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{confirmedParticipants.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 sm:gap-2 sm:px-3 sm:py-1.5"
|
||||
>
|
||||
<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]">
|
||||
{p.user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-xs font-medium text-neutral-700 sm:text-sm">
|
||||
{p.user.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{confirmedParticipants.map((p) => {
|
||||
const interests = p.user.profile?.interests ?? [];
|
||||
const city = p.user.profile?.city;
|
||||
return (
|
||||
<li key={p.id}>
|
||||
<Link
|
||||
href={`/u/${p.user.id}`}
|
||||
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"
|
||||
>
|
||||
{p.user.image ? (
|
||||
<Image
|
||||
src={p.user.image}
|
||||
alt=""
|
||||
width={40}
|
||||
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>
|
||||
|
||||
+33
-2
@@ -1,11 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.service";
|
||||
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||
import { TripCard } from "@/features/trip/components/trip-card";
|
||||
import { TripFilter } from "@/features/trip/components/trip-filter";
|
||||
import { siteConfig } from "@/lib/site";
|
||||
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 {
|
||||
searchParams: Promise<{
|
||||
@@ -13,6 +23,8 @@ interface TripsPageProps {
|
||||
from?: string;
|
||||
to?: string;
|
||||
category?: string;
|
||||
vibe?: string;
|
||||
groupSize?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -44,19 +56,30 @@ export async function generateMetadata({
|
||||
export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||
const params = await searchParams;
|
||||
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 = {
|
||||
q: params.q,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
category,
|
||||
vibe,
|
||||
groupSize,
|
||||
};
|
||||
|
||||
const [trips, allTrips] = await Promise.all([
|
||||
const session = await getServerSession(authOptions);
|
||||
const [trips, allTrips, viewerProfile] = await Promise.all([
|
||||
tripService.getOpenTrips(filters),
|
||||
hasFilters ? tripService.getOpenTrips() : null,
|
||||
session?.user?.id
|
||||
? profileRepo.findByUserId(session.user.id)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const totalCount = hasFilters ? allTrips!.length : trips.length;
|
||||
const viewerInterests = viewerProfile?.interests ?? [];
|
||||
|
||||
return (
|
||||
<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}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
@@ -133,6 +157,13 @@ export default async function TripsPage({ searchParams }: TripsPageProps) {
|
||||
isVerifiedOrganizer={
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { notFound } from "next/navigation";
|
||||
import { profileService } from "@/server/services/profile.service";
|
||||
import { TripCard } from "@/features/trip/components/trip-card";
|
||||
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||
import { siteConfig } from "@/lib/site";
|
||||
import { vibeMeta } from "@/lib/vibe";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const data = await profileService.getPublicProfile(id);
|
||||
if (!data) {
|
||||
return { title: "Profil tidak ditemukan", robots: { index: false } };
|
||||
}
|
||||
const { user } = data;
|
||||
const title = `${user.name} — Profil`;
|
||||
const desc =
|
||||
user.profile?.bio?.slice(0, 160) ||
|
||||
`Lihat profil ${user.name} di ${siteConfig.name}: trip yang dibuat, trip yang diikuti, dan minat aktivitas.`;
|
||||
return {
|
||||
title,
|
||||
description: desc,
|
||||
alternates: { canonical: `/u/${id}` },
|
||||
openGraph: { title, description: desc, url: `/u/${id}` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PublicProfilePage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const data = await profileService.getPublicProfile(id);
|
||||
if (!data) notFound();
|
||||
|
||||
const { user, isVerifiedOrganizer, organizedTrips, joinedTrips } = data;
|
||||
const profile = user.profile;
|
||||
const memberSince = new Date(user.createdAt).toLocaleDateString("id-ID", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl px-4 py-6 sm:py-8">
|
||||
{/* Header */}
|
||||
<section className="rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-5">
|
||||
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-full bg-neutral-200 sm:h-24 sm:w-24">
|
||||
{user.image ? (
|
||||
<Image
|
||||
src={user.image}
|
||||
alt={user.name}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-2xl font-bold text-neutral-500">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-xl font-bold text-neutral-800 sm:text-2xl">
|
||||
{user.name}
|
||||
</h1>
|
||||
{isVerifiedOrganizer && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-bold uppercase tracking-wide text-primary-800"
|
||||
title="Identitas organizer telah diverifikasi (KTP & rekening)"
|
||||
>
|
||||
✅ Verified Organizer
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-neutral-500">
|
||||
{profile?.city && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
📍 {profile.city}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs">Bergabung sejak {memberSince}</span>
|
||||
</div>
|
||||
|
||||
{profile?.bio && (
|
||||
<p className="mt-3 whitespace-pre-line text-sm text-neutral-700">
|
||||
{profile.bio}
|
||||
</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 && (
|
||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||
{profile.interests.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-secondary-50 px-2.5 py-0.5 text-xs font-medium text-secondary-700"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile?.instagram && (
|
||||
<a
|
||||
href={`https://instagram.com/${profile.instagram}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
<span>📸</span>
|
||||
<span>@{profile.instagram}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-2 gap-3 border-t border-neutral-100 pt-4 text-center sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-lg font-bold text-primary-600">
|
||||
{organizedTrips.length}
|
||||
</p>
|
||||
<p className="text-[11px] text-neutral-500">Trip dibuat</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-bold text-secondary-600">
|
||||
{joinedTrips.length}
|
||||
</p>
|
||||
<p className="text-[11px] text-neutral-500">Trip diikuti</p>
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-1">
|
||||
<p className="text-lg font-bold text-neutral-700">
|
||||
{organizedTrips.length + joinedTrips.length}
|
||||
</p>
|
||||
<p className="text-[11px] text-neutral-500">Total perjalanan</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Empty profile hint */}
|
||||
{!profile && (
|
||||
<p className="mt-5 rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-3 text-center text-xs text-neutral-500">
|
||||
{user.name} belum melengkapi profil sosial — bio, kota, & minat akan
|
||||
muncul di sini setelah diisi.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Trip dibuat */}
|
||||
{organizedTrips.length > 0 && (
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
|
||||
Trip yang dibuat ({organizedTrips.length})
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{organizedTrips.map((trip) => (
|
||||
<TripCard
|
||||
key={trip.id}
|
||||
id={trip.id}
|
||||
title={trip.title}
|
||||
category={trip.category}
|
||||
vibe={trip.vibe}
|
||||
destination={trip.destination}
|
||||
location={trip.location}
|
||||
date={trip.date}
|
||||
endDate={trip.endDate}
|
||||
price={trip.price}
|
||||
maxParticipants={trip.maxParticipants}
|
||||
participantCount={trip._count.participants}
|
||||
organizerName={user.name}
|
||||
status={trip.status}
|
||||
coverImage={trip.images[0]?.url}
|
||||
isVerifiedOrganizer={isVerifiedOrganizer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Trip diikuti */}
|
||||
{joinedTrips.length > 0 && (
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-3 text-base font-bold text-neutral-800 sm:text-lg">
|
||||
Trip yang diikuti ({joinedTrips.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{joinedTrips.map((trip) => (
|
||||
<li key={trip.id}>
|
||||
<ProfileTripRow
|
||||
href={`/trips/${trip.id}`}
|
||||
title={trip.title}
|
||||
destination={trip.destination}
|
||||
date={trip.date}
|
||||
endDate={trip.endDate}
|
||||
rightSlot={
|
||||
<span className="text-neutral-500">
|
||||
bareng{" "}
|
||||
<Link
|
||||
href={`/u/${trip.organizer.id}`}
|
||||
className="font-medium text-neutral-700 hover:text-primary-600"
|
||||
>
|
||||
{trip.organizer.name}
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{organizedTrips.length === 0 && joinedTrips.length === 0 && (
|
||||
<p className="mt-8 rounded-xl border border-dashed border-neutral-200 bg-white px-4 py-10 text-center text-sm text-neutral-500">
|
||||
Belum ada trip yang dibuat atau diikuti.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -21,7 +21,7 @@ export default async function VerifyPage() {
|
||||
birthDate: verification.birthDate,
|
||||
address: verification.address,
|
||||
ktpImageKey: verification.ktpImageKey,
|
||||
selfieKey: verification.selfieKey,
|
||||
livenessKey: verification.livenessKey,
|
||||
bankName: verification.bankName,
|
||||
bankAccountNumber: verification.bankAccountNumber,
|
||||
bankAccountName: verification.bankAccountName,
|
||||
|
||||
@@ -35,6 +35,12 @@ export function Navbar() {
|
||||
>
|
||||
Open Trip
|
||||
</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 ? (
|
||||
<>
|
||||
@@ -125,6 +131,13 @@ export function Navbar() {
|
||||
>
|
||||
Open Trip
|
||||
</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 ? (
|
||||
<>
|
||||
|
||||
@@ -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://`).
|
||||
+1
-1
@@ -4,7 +4,7 @@ NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_SITE_URL="https://arifal.imola.ai"
|
||||
ADMIN_EMAILS=admin@setrip.id
|
||||
|
||||
# 32-byte key (hex) for AES-256-GCM encryption of KYC data (NIK + KTP/selfie files)
|
||||
# 32-byte key (hex) for AES-256-GCM encryption of KYC data (NIK + KTP/liveness files)
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
KYC_ENCRYPTION_KEY=
|
||||
# 32-byte hex secret used as HMAC pepper for NIK uniqueness lookup
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function submitVerificationAction(formData: FormData) {
|
||||
birthDate: formData.get("birthDate") as string,
|
||||
address: formData.get("address") as string,
|
||||
ktpImageKey: formData.get("ktpImageKey") as string,
|
||||
selfieKey: formData.get("selfieKey") as string,
|
||||
livenessKey: formData.get("livenessKey") as string,
|
||||
bankName: formData.get("bankName") as string,
|
||||
bankAccountNumber: formData.get("bankAccountNumber") as string,
|
||||
bankAccountName: formData.get("bankAccountName") as string,
|
||||
|
||||
@@ -93,8 +93,8 @@ export function ReviewCard({ verification }: { verification: Verification }) {
|
||||
src={`/api/files/kyc/${verification.id}/ktp`}
|
||||
/>
|
||||
<ImagePreview
|
||||
label="Selfie + KTP"
|
||||
src={`/api/files/kyc/${verification.id}/selfie`}
|
||||
label="Foto memegang kertas SETRIP"
|
||||
src={`/api/files/kyc/${verification.id}/liveness`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ type Initial = {
|
||||
birthDate: Date;
|
||||
address: string;
|
||||
ktpImageKey: string;
|
||||
selfieKey: string;
|
||||
livenessKey: string;
|
||||
bankName: string;
|
||||
bankAccountNumber: string;
|
||||
bankAccountName: string;
|
||||
} | null;
|
||||
|
||||
type UploadKind = "ktp" | "selfie";
|
||||
type UploadKind = "ktp" | "liveness";
|
||||
|
||||
const ACCEPT_MIME = "image/jpeg,image/png,image/webp";
|
||||
const MAX_BYTES = 5 * 1024 * 1024;
|
||||
@@ -33,19 +33,19 @@ export function VerifyForm({ initial }: { initial: Initial }) {
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [ktpKey, setKtpKey] = useState(initial?.ktpImageKey ?? "");
|
||||
const [selfieKey, setSelfieKey] = useState(initial?.selfieKey ?? "");
|
||||
const [livenessKey, setLivenessKey] = useState(initial?.livenessKey ?? "");
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
if (!ktpKey || !selfieKey) {
|
||||
setError("Foto KTP dan selfie wajib diunggah");
|
||||
if (!ktpKey || !livenessKey) {
|
||||
setError("Foto KTP dan foto memegang kertas SETRIP wajib diunggah");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const formData = new FormData(e.currentTarget);
|
||||
formData.set("ktpImageKey", ktpKey);
|
||||
formData.set("selfieKey", selfieKey);
|
||||
formData.set("livenessKey", livenessKey);
|
||||
const result = await submitVerificationAction(formData);
|
||||
setLoading(false);
|
||||
if (result.error) {
|
||||
@@ -143,13 +143,22 @@ export function VerifyForm({ initial }: { initial: Initial }) {
|
||||
onChange={setKtpKey}
|
||||
onError={setError}
|
||||
/>
|
||||
<FileUpload
|
||||
label="Selfie dengan KTP"
|
||||
kind="selfie"
|
||||
value={selfieKey}
|
||||
onChange={setSelfieKey}
|
||||
onError={setError}
|
||||
/>
|
||||
<div>
|
||||
<FileUpload
|
||||
label="Foto kamu memegang kertas tulisan SETRIP"
|
||||
kind="liveness"
|
||||
value={livenessKey}
|
||||
onChange={setLivenessKey}
|
||||
onError={setError}
|
||||
/>
|
||||
<p className="mt-1.5 text-[11px] leading-relaxed text-neutral-500">
|
||||
Tulis kata <span className="font-semibold">SETRIP</span> dengan
|
||||
tangan di selembar kertas, lalu foto diri kamu sambil memegang
|
||||
kertas itu — pastikan wajah & tulisan terlihat jelas dalam satu
|
||||
foto. Foto ini bukti bahwa pengajuan benar dilakukan oleh kamu
|
||||
sendiri.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -26,10 +26,13 @@ export const submitVerificationSchema = z.object({
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^ktp\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto KTP wajib diunggah"),
|
||||
selfieKey: z
|
||||
livenessKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^selfie\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto selfie wajib diunggah"),
|
||||
.regex(
|
||||
/^liveness\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/,
|
||||
"Foto memegang kertas SETRIP wajib diunggah"
|
||||
),
|
||||
bankName: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { profileService } from "@/server/services/profile.service";
|
||||
import { updateProfileSchema } from "./schemas";
|
||||
|
||||
export async function updateProfileAction(formData: FormData) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return { error: "Kamu harus login terlebih dahulu" };
|
||||
}
|
||||
|
||||
const interests = formData
|
||||
.getAll("interests")
|
||||
.map((v) => (v as string).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const raw = {
|
||||
bio: formData.get("bio"),
|
||||
city: formData.get("city"),
|
||||
instagram: formData.get("instagram"),
|
||||
interests,
|
||||
vibe: formData.get("vibe"),
|
||||
};
|
||||
|
||||
const parsed = updateProfileSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.issues[0].message };
|
||||
}
|
||||
|
||||
try {
|
||||
await profileService.updateProfile(session.user.id, parsed.data);
|
||||
revalidatePath("/profile");
|
||||
revalidatePath(`/u/${session.user.id}`);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updateProfileAction } from "@/features/profile/actions";
|
||||
import { LIMITS } from "@/lib/limits";
|
||||
import { VIBES, vibeMeta } from "@/lib/vibe";
|
||||
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||
|
||||
interface ProfileEditorProps {
|
||||
userId: string;
|
||||
initial: {
|
||||
bio: string | null;
|
||||
city: string | null;
|
||||
interests: string[];
|
||||
instagram: string | null;
|
||||
vibe: Vibe | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function ProfileEditor({ userId, initial }: ProfileEditorProps) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(initial === null);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState(initial?.bio ?? "");
|
||||
const [city, setCity] = useState(initial?.city ?? "");
|
||||
const [instagram, setInstagram] = useState(initial?.instagram ?? "");
|
||||
const [interests, setInterests] = useState<string[]>(initial?.interests ?? []);
|
||||
const [interestDraft, setInterestDraft] = useState("");
|
||||
const [vibe, setVibe] = useState<Vibe | null>(initial?.vibe ?? null);
|
||||
|
||||
function addInterest() {
|
||||
const v = interestDraft.trim().toLowerCase();
|
||||
if (!v) return;
|
||||
if (interests.includes(v)) {
|
||||
setInterestDraft("");
|
||||
return;
|
||||
}
|
||||
if (interests.length >= LIMITS.MAX_PROFILE_INTERESTS_COUNT) {
|
||||
setError(`Maksimal ${LIMITS.MAX_PROFILE_INTERESTS_COUNT} minat`);
|
||||
return;
|
||||
}
|
||||
setInterests([...interests, v]);
|
||||
setInterestDraft("");
|
||||
setError("");
|
||||
}
|
||||
|
||||
function removeInterest(tag: string) {
|
||||
setInterests(interests.filter((t) => t !== tag));
|
||||
}
|
||||
|
||||
function handleInterestKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
addInterest();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSuccess("");
|
||||
setLoading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
if (bio.trim()) formData.set("bio", bio.trim());
|
||||
if (city.trim()) formData.set("city", city.trim());
|
||||
if (instagram.trim()) formData.set("instagram", instagram.trim());
|
||||
if (vibe) formData.set("vibe", vibe);
|
||||
interests.forEach((t) => formData.append("interests", t));
|
||||
|
||||
const result = await updateProfileAction(formData);
|
||||
setLoading(false);
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
} else {
|
||||
setSuccess("Profil berhasil disimpan");
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-neutral-200 bg-white p-4 shadow-sm sm:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-sm font-bold text-neutral-800 sm:text-base">
|
||||
Profil sosial
|
||||
</h2>
|
||||
<p className="mt-0.5 truncate text-xs text-neutral-500">
|
||||
{initial?.city || initial?.bio
|
||||
? "Profil terisi — klik untuk edit"
|
||||
: "Lengkapi profil supaya orang lain mengenalmu"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<a
|
||||
href={`/u/${userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-lg border border-neutral-200 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:bg-neutral-50"
|
||||
>
|
||||
Lihat publik ↗
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded-lg bg-primary-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-primary-700"
|
||||
>
|
||||
Edit profil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-bold text-neutral-800 sm:text-lg">
|
||||
Edit profil sosial
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-xs font-medium text-neutral-500 hover:text-neutral-700"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-4 rounded-xl bg-secondary-50 px-4 py-3 text-sm font-medium text-secondary-700">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="mb-1.5 block text-sm font-semibold text-neutral-700"
|
||||
>
|
||||
Bio singkat
|
||||
</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={LIMITS.MAX_PROFILE_BIO_LENGTH}
|
||||
placeholder="Cerita singkat tentang kamu — vibe, gaya jalan, hal yang kamu cari di trip bareng..."
|
||||
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
||||
/>
|
||||
<p className="mt-1 text-right text-[11px] text-neutral-400">
|
||||
{bio.length}/{LIMITS.MAX_PROFILE_BIO_LENGTH}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="city"
|
||||
className="mb-1.5 block text-sm font-semibold text-neutral-700"
|
||||
>
|
||||
Kota
|
||||
</label>
|
||||
<input
|
||||
id="city"
|
||||
type="text"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
maxLength={LIMITS.MAX_PROFILE_CITY_LENGTH}
|
||||
placeholder="Bandung"
|
||||
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="instagram"
|
||||
className="mb-1.5 block text-sm font-semibold text-neutral-700"
|
||||
>
|
||||
Instagram <span className="text-xs font-normal text-neutral-400">(opsional)</span>
|
||||
</label>
|
||||
<div className="flex items-center rounded-xl border border-neutral-200 bg-neutral-50 px-3 focus-within:bg-white">
|
||||
<span className="text-sm text-neutral-400">@</span>
|
||||
<input
|
||||
id="instagram"
|
||||
type="text"
|
||||
value={instagram}
|
||||
onChange={(e) =>
|
||||
setInstagram(e.target.value.replace(/^@/, ""))
|
||||
}
|
||||
maxLength={LIMITS.MAX_PROFILE_INSTAGRAM_LENGTH}
|
||||
placeholder="username"
|
||||
className="w-full bg-transparent py-2.5 pl-1 text-sm text-neutral-800 placeholder:text-neutral-400 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="interest-input"
|
||||
className="mb-1.5 block text-sm font-semibold text-neutral-700"
|
||||
>
|
||||
Minat aktivitas{" "}
|
||||
<span className="text-xs font-normal text-neutral-400">
|
||||
({interests.length}/{LIMITS.MAX_PROFILE_INTERESTS_COUNT})
|
||||
</span>
|
||||
</label>
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
{interests.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-secondary-50 px-2.5 py-0.5 text-xs font-medium text-secondary-700"
|
||||
>
|
||||
#{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeInterest(tag)}
|
||||
className="text-secondary-500 hover:text-red-600"
|
||||
aria-label={`Hapus ${tag}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id="interest-input"
|
||||
type="text"
|
||||
value={interestDraft}
|
||||
onChange={(e) => setInterestDraft(e.target.value)}
|
||||
onKeyDown={handleInterestKeyDown}
|
||||
maxLength={LIMITS.MAX_PROFILE_INTEREST_LENGTH}
|
||||
placeholder="hiking, fotografi, yoga... (Enter untuk tambah)"
|
||||
className="flex-1 rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addInterest}
|
||||
disabled={
|
||||
interests.length >= LIMITS.MAX_PROFILE_INTERESTS_COUNT
|
||||
}
|
||||
className="rounded-xl border border-neutral-200 px-3 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
+ Tambah
|
||||
</button>
|
||||
</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">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-primary-600 px-5 py-2.5 text-sm font-bold text-white shadow-md shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Menyimpan..." : "Simpan profil"}
|
||||
</button>
|
||||
<a
|
||||
href={`/u/${userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-xl border border-neutral-200 px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
||||
>
|
||||
Lihat publik ↗
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { z } from "zod/v4";
|
||||
import { LIMITS } from "@/lib/limits";
|
||||
import { VIBES } from "@/lib/vibe";
|
||||
|
||||
const optionalTrimmed = (max: number, label: string) =>
|
||||
z.preprocess(
|
||||
(val) => {
|
||||
if (val == null) return undefined;
|
||||
const s = String(val).trim();
|
||||
return s === "" ? undefined : s;
|
||||
},
|
||||
z.string().max(max, `${label} maksimal ${max} karakter`).optional()
|
||||
);
|
||||
|
||||
const interestSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, "Minat minimal 2 karakter")
|
||||
.max(
|
||||
LIMITS.MAX_PROFILE_INTEREST_LENGTH,
|
||||
`Setiap minat maksimal ${LIMITS.MAX_PROFILE_INTEREST_LENGTH} karakter`
|
||||
)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9 \-]+$/,
|
||||
"Minat hanya boleh huruf, angka, spasi, atau strip"
|
||||
);
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
bio: optionalTrimmed(LIMITS.MAX_PROFILE_BIO_LENGTH, "Bio"),
|
||||
city: optionalTrimmed(LIMITS.MAX_PROFILE_CITY_LENGTH, "Kota"),
|
||||
instagram: z.preprocess(
|
||||
(val) => {
|
||||
if (val == null) return undefined;
|
||||
const s = String(val).trim().replace(/^@/, "");
|
||||
return s === "" ? undefined : s;
|
||||
},
|
||||
z
|
||||
.string()
|
||||
.max(
|
||||
LIMITS.MAX_PROFILE_INSTAGRAM_LENGTH,
|
||||
`Instagram maksimal ${LIMITS.MAX_PROFILE_INSTAGRAM_LENGTH} karakter`
|
||||
)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9._]+$/,
|
||||
"Username Instagram hanya boleh huruf, angka, titik, atau underscore"
|
||||
)
|
||||
.optional()
|
||||
),
|
||||
interests: z
|
||||
.array(interestSchema)
|
||||
.max(
|
||||
LIMITS.MAX_PROFILE_INTERESTS_COUNT,
|
||||
`Maksimal ${LIMITS.MAX_PROFILE_INTERESTS_COUNT} minat`
|
||||
)
|
||||
.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>;
|
||||
@@ -28,6 +28,7 @@ export async function createTripAction(formData: FormData) {
|
||||
endDate: (formData.get("endDate") as string) || undefined,
|
||||
maxParticipants: formData.get("maxParticipants") as string,
|
||||
price: formData.get("price") as string,
|
||||
vibe: formData.get("vibe"),
|
||||
};
|
||||
|
||||
const result = createTripSchema.safeParse(raw);
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
ACTIVITY_CATEGORIES,
|
||||
categoryMeta,
|
||||
} 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 {
|
||||
const num = value.replace(/\D/g, "");
|
||||
@@ -32,6 +33,7 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [category, setCategory] = useState<ActivityCategory>("HIKING");
|
||||
const [vibe, setVibe] = useState<Vibe | null>(null);
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [priceDisplay, setPriceDisplay] = useState("");
|
||||
@@ -62,6 +64,7 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
}
|
||||
}
|
||||
formData.set("price", parseRupiahInput(priceDisplay));
|
||||
if (vibe) formData.set("vibe", vibe);
|
||||
|
||||
const result = await createTripAction(formData);
|
||||
|
||||
@@ -124,6 +127,57 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
<input type="hidden" name="category" value={category} />
|
||||
</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>
|
||||
<label htmlFor="title" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||
Judul Trip
|
||||
|
||||
@@ -3,12 +3,21 @@ import Link from "next/link";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||
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 {
|
||||
id: string;
|
||||
title: string;
|
||||
category: ActivityCategory;
|
||||
vibe?: Vibe | null;
|
||||
destination: string;
|
||||
location: string;
|
||||
date: Date | string;
|
||||
@@ -21,12 +30,17 @@ interface TripCardProps {
|
||||
coverImage?: string | null;
|
||||
priority?: 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({
|
||||
id,
|
||||
title,
|
||||
category,
|
||||
vibe,
|
||||
destination,
|
||||
location,
|
||||
date,
|
||||
@@ -39,10 +53,25 @@ export function TripCard({
|
||||
coverImage,
|
||||
priority,
|
||||
isVerifiedOrganizer,
|
||||
participants,
|
||||
viewerInterests,
|
||||
}: TripCardProps) {
|
||||
const spotsLeft = maxParticipants - participantCount;
|
||||
const isSmallGroup = maxParticipants <= 10;
|
||||
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 (
|
||||
<Link href={`/trips/${id}`} className="group block">
|
||||
@@ -63,13 +92,24 @@ export function TripCard({
|
||||
<span className="text-4xl">{meta.icon}</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
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"
|
||||
title={`Kategori: ${meta.label}`}
|
||||
>
|
||||
<span>{meta.icon}</span>
|
||||
<span>{meta.label}</span>
|
||||
</span>
|
||||
<div className="absolute left-3 top-3 flex flex-wrap gap-1">
|
||||
<span
|
||||
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>
|
||||
{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
|
||||
className={`absolute right-3 top-3 rounded-full px-2.5 py-0.5 text-xs font-bold backdrop-blur-sm ${
|
||||
status === "OPEN"
|
||||
@@ -120,6 +160,48 @@ export function TripCard({
|
||||
</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">
|
||||
<span className="text-lg font-bold text-primary-600">
|
||||
{formatRupiah(price)}
|
||||
|
||||
@@ -10,16 +10,38 @@ import {
|
||||
categoryMeta,
|
||||
isActivityCategory,
|
||||
} 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() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const initialCategory = searchParams.get("category");
|
||||
const initialVibe = searchParams.get("vibe");
|
||||
const initialGroup = searchParams.get("groupSize");
|
||||
const [category, setCategory] = useState<ActivityCategory | 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 [startDate, setStartDate] = useState<Date | 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
|
||||
);
|
||||
|
||||
function buildParams(overrides?: { category?: ActivityCategory | null }) {
|
||||
function buildParams(overrides?: {
|
||||
category?: ActivityCategory | null;
|
||||
vibe?: Vibe | null;
|
||||
groupSize?: GroupSize | null;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
const nextCategory =
|
||||
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 (nextVibe) params.set("vibe", nextVibe);
|
||||
if (nextGroup) params.set("groupSize", nextGroup);
|
||||
if (query.trim()) params.set("q", query.trim());
|
||||
if (startDate) params.set("from", formatLocalCalendarYmd(startDate));
|
||||
if (endDate) params.set("to", formatLocalCalendarYmd(endDate));
|
||||
@@ -49,6 +80,16 @@ export function TripFilter() {
|
||||
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]) {
|
||||
const [start, end] = dates;
|
||||
setStartDate(start);
|
||||
@@ -57,6 +98,8 @@ export function TripFilter() {
|
||||
if (!start && !end) {
|
||||
const params = new URLSearchParams();
|
||||
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());
|
||||
pushFilters(params);
|
||||
}
|
||||
@@ -69,13 +112,15 @@ export function TripFilter() {
|
||||
|
||||
function handleReset() {
|
||||
setCategory(null);
|
||||
setVibe(null);
|
||||
setGroupSize(null);
|
||||
setQuery("");
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
router.push("/trips");
|
||||
}
|
||||
|
||||
const hasFilters = category || query || startDate || endDate;
|
||||
const hasFilters = category || vibe || groupSize || query || startDate || endDate;
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -123,6 +168,89 @@ export function TripFilter() {
|
||||
</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">
|
||||
{/* Search input */}
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod/v4";
|
||||
import { LIMITS } from "@/lib/limits";
|
||||
import { ACTIVITY_CATEGORIES } from "@/lib/activity-category";
|
||||
import { VIBES } from "@/lib/vibe";
|
||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
||||
import {
|
||||
isTripDepartureDayPast,
|
||||
@@ -146,6 +147,14 @@ export const createTripSchema = z
|
||||
)
|
||||
.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) => {
|
||||
const dep = tripStoredInstantFromYmd(data.date);
|
||||
|
||||
@@ -19,6 +19,12 @@ export const LIMITS = {
|
||||
MAX_URL_LENGTH: 2048,
|
||||
MAX_NAME_LENGTH: 80,
|
||||
MAX_PASSWORD_LENGTH: 72,
|
||||
/** Profil sosial publik */
|
||||
MAX_PROFILE_BIO_LENGTH: 500,
|
||||
MAX_PROFILE_CITY_LENGTH: 60,
|
||||
MAX_PROFILE_INTEREST_LENGTH: 30,
|
||||
MAX_PROFILE_INTERESTS_COUNT: 10,
|
||||
MAX_PROFILE_INSTAGRAM_LENGTH: 30,
|
||||
/** Verifikasi organizer (KTP + bank) */
|
||||
MAX_ADDRESS_LENGTH: 500,
|
||||
MAX_BANK_NAME_LENGTH: 60,
|
||||
|
||||
@@ -3,11 +3,11 @@ import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import { encryptBuffer, decryptBuffer } from "@/lib/crypto";
|
||||
|
||||
export type KycKind = "ktp" | "selfie";
|
||||
export type KycKind = "ktp" | "liveness";
|
||||
|
||||
const KIND_DIRS: Record<KycKind, string> = {
|
||||
ktp: "ktp",
|
||||
selfie: "selfie",
|
||||
liveness: "liveness",
|
||||
};
|
||||
|
||||
/** Bytes. ~5MB matches the form limit; raise here if you change the upload route. */
|
||||
@@ -43,7 +43,7 @@ export type StoredFileMeta = {
|
||||
};
|
||||
|
||||
export function isKycKind(value: string): value is KycKind {
|
||||
return value === "ktp" || value === "selfie";
|
||||
return value === "ktp" || value === "liveness";
|
||||
}
|
||||
|
||||
/** Resolve a storage key (`ktp/abc.jpg`) to an absolute path inside the upload dir. Throws on traversal. */
|
||||
|
||||
+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)
|
||||
);
|
||||
}
|
||||
Generated
+47
-47
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "setrip",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "setrip",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/adapter-pg": "^7.7.0",
|
||||
@@ -1657,9 +1657,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz",
|
||||
"integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.5.tgz",
|
||||
"integrity": "sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1673,9 +1673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz",
|
||||
"integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.5.tgz",
|
||||
"integrity": "sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1689,9 +1689,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz",
|
||||
"integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.5.tgz",
|
||||
"integrity": "sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1705,9 +1705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz",
|
||||
"integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.5.tgz",
|
||||
"integrity": "sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1724,9 +1724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz",
|
||||
"integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.5.tgz",
|
||||
"integrity": "sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1743,9 +1743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz",
|
||||
"integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.5.tgz",
|
||||
"integrity": "sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1762,9 +1762,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz",
|
||||
"integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.5.tgz",
|
||||
"integrity": "sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1781,9 +1781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz",
|
||||
"integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.5.tgz",
|
||||
"integrity": "sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1797,9 +1797,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.5.tgz",
|
||||
"integrity": "sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5376,9 +5376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.14",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
|
||||
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
|
||||
"version": "4.12.18",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
|
||||
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -6563,12 +6563,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
|
||||
"integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz",
|
||||
"integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.2.4",
|
||||
"@next/env": "16.2.5",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -6582,14 +6582,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.2.4",
|
||||
"@next/swc-darwin-x64": "16.2.4",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.4",
|
||||
"@next/swc-linux-arm64-musl": "16.2.4",
|
||||
"@next/swc-linux-x64-gnu": "16.2.4",
|
||||
"@next/swc-linux-x64-musl": "16.2.4",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.4",
|
||||
"@next/swc-win32-x64-msvc": "16.2.4",
|
||||
"@next/swc-darwin-arm64": "16.2.5",
|
||||
"@next/swc-darwin-x64": "16.2.5",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.5",
|
||||
"@next/swc-linux-arm64-musl": "16.2.5",
|
||||
"@next/swc-linux-x64-gnu": "16.2.5",
|
||||
"@next/swc-linux-x64-musl": "16.2.5",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.5",
|
||||
"@next/swc-win32-x64-msvc": "16.2.5",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -7145,9 +7145,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
||||
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "setrip",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserProfile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"bio" TEXT,
|
||||
"city" TEXT,
|
||||
"interests" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"instagram" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserProfile_userId_key" ON "UserProfile"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -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");
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable: rename selfieKey -> livenessKey.
|
||||
-- Sebelumnya: storage key untuk selfie memegang KTP.
|
||||
-- Sekarang: storage key foto liveness (user memegang kertas tulisan "SETRIP").
|
||||
ALTER TABLE "OrganizerVerification" RENAME COLUMN "selfieKey" TO "livenessKey";
|
||||
+36
-2
@@ -30,6 +30,36 @@ model User {
|
||||
|
||||
organizerVerification OrganizerVerification? @relation("OrganizerVerificationOwner")
|
||||
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
|
||||
|
||||
profile UserProfile?
|
||||
}
|
||||
|
||||
/// Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
||||
/// (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
||||
model UserProfile {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Bio singkat, teks bebas
|
||||
bio String?
|
||||
/// Kota domisili (teks bebas, mis. "Bandung", "Jakarta Selatan")
|
||||
city String?
|
||||
/// Tag minat aktivitas (mis. ["hiking", "fotografi", "yoga"])
|
||||
interests String[] @default([])
|
||||
/// Username Instagram (tanpa @, opsional)
|
||||
instagram String?
|
||||
/// Gaya jalan / energi user — dipakai untuk matching teman dengan ritme serupa.
|
||||
vibe Vibe?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum Vibe {
|
||||
CHILL
|
||||
BALANCED
|
||||
HARDCORE
|
||||
}
|
||||
|
||||
/// Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
||||
@@ -69,8 +99,9 @@ model OrganizerVerification {
|
||||
|
||||
/// Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public.
|
||||
ktpImageKey String
|
||||
/// Storage key selfie memegang KTP.
|
||||
selfieKey String
|
||||
/// Storage key foto liveness — user memegang kertas bertuliskan "SETRIP".
|
||||
/// (Sebelumnya: selfie memegang KTP. Diganti supaya user tidak perlu memajang KTP dua kali.)
|
||||
livenessKey String
|
||||
|
||||
bankName String
|
||||
bankAccountNumber String
|
||||
@@ -114,6 +145,8 @@ model Trip {
|
||||
endDate DateTime?
|
||||
maxParticipants Int
|
||||
price Int
|
||||
/// Ritme/energi trip — dipakai untuk matching dengan vibe user.
|
||||
vibe Vibe?
|
||||
status TripStatus @default(OPEN)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -126,6 +159,7 @@ model Trip {
|
||||
reviews TripReview[]
|
||||
|
||||
@@index([category, status, date])
|
||||
@@index([vibe, status, date])
|
||||
}
|
||||
|
||||
model TripReview {
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ async function main() {
|
||||
birthDate: new Date(Date.UTC(1990, 0, 1)),
|
||||
address: "Jl. Pendaki No. 1, Garut, Jawa Barat",
|
||||
ktpImageKey: "ktp/seed-dede.jpg",
|
||||
selfieKey: "selfie/seed-dede.jpg",
|
||||
livenessKey: "liveness/seed-dede.jpg",
|
||||
bankName: "BCA",
|
||||
bankAccountNumber: "1234567890",
|
||||
bankAccountName: "Dede Inoen",
|
||||
@@ -85,7 +85,7 @@ async function main() {
|
||||
birthDate: new Date(Date.UTC(1985, 5, 15)),
|
||||
address: "Jl. Adventure No. 7, Kuningan, Jawa Barat",
|
||||
ktpImageKey: "ktp/seed-panji.jpg",
|
||||
selfieKey: "selfie/seed-panji.jpg",
|
||||
livenessKey: "liveness/seed-panji.jpg",
|
||||
bankName: "Mandiri",
|
||||
bankAccountNumber: "9876543210",
|
||||
bankAccountName: "Panji Petualang",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import type { Vibe } from "@/app/generated/prisma/enums";
|
||||
|
||||
interface UpsertProfileInput {
|
||||
bio?: string;
|
||||
city?: string;
|
||||
instagram?: string;
|
||||
interests: string[];
|
||||
vibe?: Vibe;
|
||||
}
|
||||
|
||||
export const profileRepo = {
|
||||
async findByUserId(userId: string) {
|
||||
return prisma.userProfile.findUnique({ where: { userId } });
|
||||
},
|
||||
|
||||
async upsertByUserId(userId: string, data: UpsertProfileInput) {
|
||||
return prisma.userProfile.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
bio: data.bio,
|
||||
city: data.city,
|
||||
instagram: data.instagram,
|
||||
interests: data.interests,
|
||||
vibe: data.vibe,
|
||||
},
|
||||
update: {
|
||||
bio: data.bio,
|
||||
city: data.city,
|
||||
instagram: data.instagram,
|
||||
interests: data.interests,
|
||||
vibe: data.vibe,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
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 {
|
||||
utcStartOfDay,
|
||||
utcDayStartFromYmd,
|
||||
@@ -8,11 +8,15 @@ import {
|
||||
maxUtcDate,
|
||||
} from "@/lib/trip-dates";
|
||||
|
||||
export type GroupSize = "SMALL" | "MEDIUM" | "LARGE";
|
||||
|
||||
export interface TripFilters {
|
||||
q?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
category?: ActivityCategory;
|
||||
vibe?: Vibe;
|
||||
groupSize?: GroupSize;
|
||||
}
|
||||
|
||||
export const tripRepo = {
|
||||
@@ -47,6 +51,18 @@ export const tripRepo = {
|
||||
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) {
|
||||
andParts.push({ date: { gte: todayStart } });
|
||||
} else {
|
||||
@@ -104,6 +120,22 @@ export const tripRepo = {
|
||||
},
|
||||
},
|
||||
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: {
|
||||
select: {
|
||||
participants: { where: { status: { not: "CANCELLED" } } },
|
||||
@@ -129,7 +161,16 @@ export const tripRepo = {
|
||||
},
|
||||
images: { orderBy: { order: "asc" } },
|
||||
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: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
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 = {
|
||||
async findByEmail(email: string) {
|
||||
@@ -24,6 +31,82 @@ export const userRepo = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Profil sosial publik untuk halaman /u/[id]. JANGAN sertakan field sensitif
|
||||
* (email, password, KYC). Hanya yang user pilih untuk dibagikan.
|
||||
*/
|
||||
async findSocialProfileById(id: string) {
|
||||
return prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
profile: {
|
||||
select: {
|
||||
bio: true,
|
||||
city: true,
|
||||
interests: true,
|
||||
instagram: true,
|
||||
vibe: true,
|
||||
},
|
||||
},
|
||||
organizerVerification: { select: { status: true } },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return prisma.user.create({ data });
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ type SubmitInput = {
|
||||
birthDate: Date;
|
||||
address: string;
|
||||
ktpImageKey: string;
|
||||
selfieKey: string;
|
||||
livenessKey: string;
|
||||
bankName: string;
|
||||
bankAccountNumber: string;
|
||||
bankAccountName: string;
|
||||
@@ -36,7 +36,7 @@ export const organizerService = {
|
||||
birthDate: data.birthDate,
|
||||
address: data.address,
|
||||
ktpImageKey: data.ktpImageKey,
|
||||
selfieKey: data.selfieKey,
|
||||
livenessKey: data.livenessKey,
|
||||
bankName: data.bankName,
|
||||
bankAccountNumber: data.bankAccountNumber,
|
||||
bankAccountName: data.bankAccountName,
|
||||
|
||||
@@ -1,10 +1,59 @@
|
||||
import { userRepo } from "@/server/repositories/user.repo";
|
||||
import { userRepo, type PeopleFilters } from "@/server/repositories/user.repo";
|
||||
import { tripRepo } from "@/server/repositories/trip.repo";
|
||||
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||
import { organizerRepo } from "@/server/repositories/organizer.repo";
|
||||
import { profileRepo } from "@/server/repositories/profile.repo";
|
||||
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
||||
import type { UpdateProfileInput } from "@/features/profile/schemas";
|
||||
|
||||
export const profileService = {
|
||||
async getOwnProfile(userId: string) {
|
||||
return profileRepo.findByUserId(userId);
|
||||
},
|
||||
|
||||
async findPeople(filters?: PeopleFilters) {
|
||||
return userRepo.findPeople(filters);
|
||||
},
|
||||
|
||||
async updateProfile(userId: string, input: UpdateProfileInput) {
|
||||
return profileRepo.upsertByUserId(userId, {
|
||||
bio: input.bio,
|
||||
city: input.city,
|
||||
instagram: input.instagram,
|
||||
interests: input.interests,
|
||||
vibe: input.vibe,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Halaman profil publik /u/[id]. Membaca user + UserProfile + counts trip.
|
||||
* Tidak ekspos email/KYC.
|
||||
*/
|
||||
async getPublicProfile(userId: string) {
|
||||
const user = await userRepo.findSocialProfileById(userId);
|
||||
if (!user) return null;
|
||||
|
||||
const [organizedTrips, participations] = await Promise.all([
|
||||
tripRepo.findByOrganizerId(userId),
|
||||
participantRepo.findWithTripForProfile(userId),
|
||||
]);
|
||||
|
||||
const joinedTrips = participations
|
||||
.filter((p) => p.status !== "CANCELLED")
|
||||
.map((p) => p.trip)
|
||||
.sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
isVerifiedOrganizer:
|
||||
user.organizerVerification?.status === "APPROVED",
|
||||
organizedTrips,
|
||||
joinedTrips,
|
||||
};
|
||||
},
|
||||
|
||||
async getProfileDashboard(userId: string) {
|
||||
const user = await userRepo.findPublicProfileById(userId);
|
||||
if (!user) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
|
||||
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||
@@ -31,6 +31,7 @@ interface CreateTripInput {
|
||||
endDate?: Date;
|
||||
maxParticipants: number;
|
||||
price: number;
|
||||
vibe?: Vibe;
|
||||
organizerId: string;
|
||||
imageUrls?: string[];
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export const tripService = {
|
||||
endDate: input.endDate,
|
||||
maxParticipants: input.maxParticipants,
|
||||
price: input.price,
|
||||
vibe: input.vibe,
|
||||
organizer: { connect: { id: input.organizerId } },
|
||||
images,
|
||||
} satisfies Prisma.TripCreateInput;
|
||||
|
||||
Reference in New Issue
Block a user