add user profile, profile vibe and trip vibe and social signal

This commit is contained in:
2026-05-08 19:20:27 +07:00
parent 3228ef712f
commit 7f419638b5
39 changed files with 1361 additions and 192 deletions
+4
View File
@@ -1,10 +1,12 @@
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 = {
@@ -21,12 +23,14 @@ export const profileRepo = {
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" },
+58
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) {
@@ -42,6 +49,7 @@ export const userRepo = {
city: true,
interests: true,
instagram: true,
vibe: true,
},
},
organizerVerification: { select: { status: true } },
@@ -49,6 +57,56 @@ export const userRepo = {
});
},
/**
* Discovery /people: ambil user yang punya profil sosial terisi (minimal salah
* satu dari bio/city/interests/vibe). Filter optional by city/interest/vibe.
* Tidak ekspos email/KYC.
*/
async findPeople(filters?: PeopleFilters, limit = 60) {
const profileWhere: Prisma.UserProfileWhereInput = {
OR: [
{ bio: { not: null } },
{ city: { not: null } },
{ vibe: { not: null } },
{ interests: { isEmpty: false } },
],
};
if (filters?.city) {
profileWhere.city = {
contains: filters.city,
mode: "insensitive",
};
}
if (filters?.interest) {
profileWhere.interests = { has: filters.interest.toLowerCase() };
}
if (filters?.vibe) {
profileWhere.vibe = filters.vibe;
}
return prisma.user.findMany({
where: { profile: { is: profileWhere } },
select: {
id: true,
name: true,
image: true,
createdAt: true,
profile: {
select: {
bio: true,
city: true,
interests: true,
vibe: true,
},
},
organizerVerification: { select: { status: true } },
},
orderBy: { createdAt: "desc" },
take: limit,
});
},
async create(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
},
+6 -1
View File
@@ -1,4 +1,4 @@
import { userRepo } from "@/server/repositories/user.repo";
import { userRepo, type PeopleFilters } from "@/server/repositories/user.repo";
import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { organizerRepo } from "@/server/repositories/organizer.repo";
@@ -11,12 +11,17 @@ export const profileService = {
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,
});
},
+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;