Compare commits

...

5 Commits

Author SHA1 Message Date
arifal ccb3437e82 change selfie with ktp to selfie with setrip tag 2026-05-08 20:02:11 +07:00
arifal f5d86d2414 0.7.0 2026-05-08 19:20:33 +07:00
arifal 7f419638b5 add user profile, profile vibe and trip vibe and social signal 2026-05-08 19:20:27 +07:00
arifal 3228ef712f 0.6.0 2026-05-08 18:24:14 +07:00
arifal 63349a144d general destination and verify 2026-05-08 18:23:51 +07:00
64 changed files with 4010 additions and 319 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"WebFetch(domain:unsplash.com)"
"WebFetch(domain:unsplash.com)",
"Bash(npx prisma *)"
]
}
}
+1 -1
View File
@@ -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
View File
@@ -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).
+2 -2
View File
@@ -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
+110
View File
@@ -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 1120 / 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".
+2 -1
View File
@@ -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>
+1 -1
View File
@@ -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 });
}
+4 -1
View File
@@ -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 });
+1 -1
View File
@@ -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
+6
View File
@@ -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.
+6
View File
@@ -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.
+34
View File
@@ -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
View File
@@ -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',
+1
View File
@@ -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 -1
View File
@@ -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'>
+160
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+104
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+239
View File
@@ -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
View File
@@ -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,
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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>
+22 -13
View File
@@ -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>
+5 -2
View File
@@ -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()
+41
View File
@@ -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>
);
}
+102
View File
@@ -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>
);
}
+66
View File
@@ -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>;
+1
View File
@@ -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);
+55 -1
View File
@@ -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
+90 -8
View File
@@ -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)}
+131 -3
View File
@@ -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: "1120" },
{ 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">
+9
View File
@@ -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);
+6
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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)
);
}
+47 -47
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+37
View File
@@ -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,
},
});
},
};
+43 -2
View File
@@ -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" },
+83
View File
@@ -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 });
},
+2 -2
View File
@@ -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,
+50 -1
View File
@@ -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) {
+3 -1
View File
@@ -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;