From 078893f7d1895dbf0bd7e2615f8ea517dfd79c90 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Mon, 11 Aug 2025 17:38:26 +0200 Subject: [PATCH] Add `books` feature --- app/api/interests/route.ts | 21 +++++++++++++----- app/api/profiles/route.ts | 29 ++++++++++++++++++++++++ app/api/user/update-profile/route.ts | 27 +++++++++++++++-------- app/complete-profile/page.tsx | 22 ++++++++++++++++--- app/profiles/ProfileFilters.tsx | 1 + app/profiles/page.tsx | 14 ++++++++++++ lib/client/profile.tsx | 8 ++++--- lib/client/schema.ts | 3 ++- lib/server/db-utils.ts | 1 + prisma/schema.prisma | 18 ++++++++++++++- prisma/seed.ts | 33 +++++++++++++++++++++++----- 11 files changed, 150 insertions(+), 27 deletions(-) diff --git a/app/api/interests/route.ts b/app/api/interests/route.ts index f914c85a..5f860905 100644 --- a/app/api/interests/route.ts +++ b/app/api/interests/route.ts @@ -1,5 +1,5 @@ -import { prisma } from "@/lib/server/prisma"; -import { NextResponse } from "next/server"; +import {prisma} from "@/lib/server/prisma"; +import {NextResponse} from "next/server"; export async function GET() { try { @@ -28,6 +28,17 @@ export async function GET() { cacheStrategy: cacheStrategy, }); + const books = await prisma.book.findMany({ + select: { + id: true, + name: true, + }, + orderBy: { + name: 'asc' + }, + cacheStrategy: cacheStrategy, + }); + const causeAreas = await prisma.causeArea.findMany({ select: { id: true, @@ -50,12 +61,12 @@ export async function GET() { cacheStrategy: cacheStrategy, }); - return NextResponse.json({ interests, coreValues, causeAreas, connections }); + return NextResponse.json({interests, coreValues, books, causeAreas, connections}); } catch (error) { console.error('Error fetching interests:', error); return NextResponse.json( - { error: "Failed to fetch interests" }, - { status: 500 } + {error: "Failed to fetch interests"}, + {status: 500} ); } } diff --git a/app/api/profiles/route.ts b/app/api/profiles/route.ts index 35ec155f..b822cb2d 100644 --- a/app/api/profiles/route.ts +++ b/app/api/profiles/route.ts @@ -12,6 +12,7 @@ export async function GET(request: Request) { const maxIntroversion = url.searchParams.get("maxIntroversion"); const interests = url.searchParams.get("interests")?.split(",").filter(Boolean) || []; const coreValues = url.searchParams.get("coreValues")?.split(",").filter(Boolean) || []; + const books = url.searchParams.get("books")?.split(",").filter(Boolean) || []; const causeAreas = url.searchParams.get("causeAreas")?.split(",").filter(Boolean) || []; const connections = url.searchParams.get("connections")?.split(",").filter(Boolean) || []; const searchQueries = url.searchParams.get("searchQuery")?.split(",").map(q => q.trim()).filter(Boolean) || []; @@ -116,6 +117,22 @@ export async function GET(request: Request) { ]; } + // AND + if (books.length > 0) { + where.profile.AND = [ + ...where.profile.AND, + ...books.map((name) => ({ + books: { + some: { + value: { + name: name, + }, + }, + }, + })), + ]; + } + if (causeAreas.length > 0) { where.profile.AND = [ ...where.profile.AND, @@ -194,6 +211,17 @@ export async function GET(request: Request) { }, }, }, + { + profile: { + books: { + some: { + value: { + name: {contains: query, mode: "insensitive"}, + }, + }, + }, + }, + }, { profile: { causeAreas: { @@ -259,6 +287,7 @@ export async function GET(request: Request) { include: { intellectualInterests: {include: {interest: true}}, coreValues: {include: {value: true}}, + books: {include: {value: true}}, causeAreas: {include: {causeArea: true}}, desiredConnections: {include: {connection: true}}, promptAnswers: true, diff --git a/app/api/user/update-profile/route.ts b/app/api/user/update-profile/route.ts index 9a26d8e1..42bd8a10 100644 --- a/app/api/user/update-profile/route.ts +++ b/app/api/user/update-profile/route.ts @@ -14,8 +14,9 @@ export async function POST(req: Request) { } const data = await req.json(); - const {profile, image, name, interests = [], connections = [], coreValues = [], causeAreas = []} = data; + const {profile, image, name, interests = [], connections = [], coreValues = [], books = [], causeAreas = []} = data; + console.log('books: ', books) Object.keys(profile).forEach(key => { if (profile[key] === '' || !profile[key]) { delete profile[key]; @@ -71,6 +72,8 @@ export async function POST(req: Request) { profileConnection: prisma.profileConnection, value: prisma.value, profileValue: prisma.profileValue, + book: prisma.book, + profileBook: prisma.profileBook, causeArea: prisma.causeArea, profileCauseArea: prisma.profileCauseArea, } as const; @@ -79,7 +82,7 @@ export async function POST(req: Request) { async function handleFeatures(features: any, attribute: ModelKey, profileAttribute: string, idName: string) { // Add new features - if (features.length > 0 && updatedUser.profile) { + if (features !== null && updatedUser.profile) { // First, find or create all features console.log('profile', profileAttribute, profileAttribute); const operations = features.map((feat: { id?: string; name: string }) => @@ -95,25 +98,31 @@ export async function POST(req: Request) { // Get the IDs of all created/updated features const ids = createdFeatures.map(v => v.id); - // First, remove all existing interests for this profile - await modelMap[profileAttribute].deleteMany({ - where: {profileId: updatedUser.profile.id}, - }); + const profileId = updatedUser.profile.id; + console.log('profile ID:', profileId); - // Then, create new connections + // First, remove all existing features for this profile + const res = await modelMap[profileAttribute].deleteMany({ + where: {profileId: profileId}, + }); + console.log('deleted profile:', profileAttribute, res); + + // Then, create new features if (ids.length > 0) { - await modelMap[profileAttribute].createMany({ + const create_res =await modelMap[profileAttribute].createMany({ data: ids.map(id => ({ - profileId: updatedUser.profile!.id, + profileId: profileId, [idName]: id, })), skipDuplicates: true, }); + console.log('created many:', profileAttribute, create_res); } } } await handleFeatures(interests, 'interest', 'profileInterest', 'interestId') + await handleFeatures(books, 'book', 'profileBook', 'valueId') await handleFeatures(connections, 'connection', 'profileConnection', 'connectionId') await handleFeatures(coreValues, 'value', 'profileValue', 'valueId') await handleFeatures(causeAreas, 'causeArea', 'profileCauseArea', 'causeAreaId') diff --git a/app/complete-profile/page.tsx b/app/complete-profile/page.tsx index a889397e..bc720cc5 100644 --- a/app/complete-profile/page.tsx +++ b/app/complete-profile/page.tsx @@ -56,7 +56,7 @@ function RegisterComponent() { const router = useRouter(); const {data: session, update} = useSession(); - const featureNames = ['interests', 'coreValues', 'description', 'connections', 'causeAreas']; + const featureNames = ['interests', 'coreValues', 'description', 'connections', 'causeAreas', 'books']; const [showMoreInfo, _setShowMoreInfo] = useState(() => Object.fromEntries(featureNames.map((id) => [id, false])) @@ -141,6 +141,7 @@ function RegisterComponent() { setSelFeat('coreValues', 'coreValues', 'value') setSelFeat('connections', 'desiredConnections', 'connection') setSelFeat('causeAreas', 'causeAreas', 'causeArea') + setSelFeat('books', 'books', 'value') setImages([]) setKeys(profile?.images) @@ -309,10 +310,11 @@ function RegisterComponent() { ...(key && {image: key}), ...(name && {name}), }; - for (const name of ['interests', 'connections', 'coreValues', 'causeAreas']) { + for (const name of ['books', 'interests', 'connections', 'coreValues', 'causeAreas']) { + // if (!selectedFeatures[name].size) continue; data[name] = Array.from(selectedFeatures[name]).map(id => ({ id: id.startsWith('new-') ? undefined : id, - name: allFeatures[name].find(i => i.id === id)?.name || id.replace('new-', '') + name: allFeatures[name].find(i => i.id === id)?.name })); } console.log('data', data) @@ -416,6 +418,20 @@ function RegisterComponent() {

}, + { + id: 'books', title: 'Works to discuss', allowAdd: true, + content: <> +

+ List the works (books, articles, essays, reports, etc.) you would like to bring up. + For each work, include the exact title (as it appears on the cover), the + author’s full name, and, if necessary, the edition or publication year. For example: Peter Singer - Animal + Liberation. If you want to focus on specific + chapters, themes, or questions, note them in your description—it helps keep the discussion targeted. Don’t just write + “something by Orwell” or “that new mystery”; vague entries waste time and make it harder for others to find + the right work. Be explicit so everyone is literally on the same page! +

+ + }, // { // id: 'causeAreas', title: 'Cause Areas', allowAdd: true, // content: <> diff --git a/app/profiles/ProfileFilters.tsx b/app/profiles/ProfileFilters.tsx index 33094a17..fa36d831 100644 --- a/app/profiles/ProfileFilters.tsx +++ b/app/profiles/ProfileFilters.tsx @@ -31,6 +31,7 @@ export const dropdownConfig: { id: DropdownKey, name: string }[] = [ {id: "connections", name: "Connection Type"}, {id: "coreValues", name: "Values"}, {id: "interests", name: "Interests"}, + {id: "books", name: "Works"}, // {id: "causeAreas", name: "Cause Areas"}, ] diff --git a/app/profiles/page.tsx b/app/profiles/page.tsx index ec5b1dc2..ab5d1709 100644 --- a/app/profiles/page.tsx +++ b/app/profiles/page.tsx @@ -20,6 +20,7 @@ const initialState = { maxIntroversion: null as number | null, interests: [] as string[], coreValues: [] as string[], + books: [] as string[], causeAreas: [] as string[], connections: [] as string[], searchQuery: '', @@ -33,6 +34,7 @@ type ProfileFilters = { minIntroversion: number | null; maxIntroversion: number | null; interests: string[]; + books: string[]; coreValues: string[]; causeAreas: string[]; connections: string[]; @@ -327,6 +329,18 @@ export default function ProfilePage() { )} +
+ {user.profile.books?.length > 0 && ( +
+ {user.profile.books.slice(0, 6).map(({value}) => ( + + {value?.name} + + ))} +
+ )} +
))} diff --git a/lib/client/profile.tsx b/lib/client/profile.tsx index a1723ecb..0bbb17f2 100644 --- a/lib/client/profile.tsx +++ b/lib/client/profile.tsx @@ -115,19 +115,21 @@ export function Profile(url: string, header: any = null) { interface Tags { profileAttribute: string; - attribute: string; + attribute?: string; title: string; } const tagsConfig: Tags[] = [ {profileAttribute: 'desiredConnections', attribute: 'connection', title: 'Connection Type'}, - {profileAttribute: 'coreValues', attribute: 'value', title: 'Values'}, + {profileAttribute: 'coreValues', title: 'Values'}, {profileAttribute: 'intellectualInterests', attribute: 'interest', title: 'Interests'}, + {profileAttribute: 'books', title: 'Works to Discuss'}, // {profileAttribute: 'causeAreas', attribute: 'causeArea', title: 'Cause Areas'}, ] - function getTags({profileAttribute, attribute, title}: Tags) { + function getTags({profileAttribute, attribute = 'value', title}: Tags) { const values = userData?.profile?.[profileAttribute]; + console.log('values', values); return
{values?.length > 0 && (
< diff --git a/lib/client/schema.ts b/lib/client/schema.ts index ddce65c6..fbfbb1d2 100644 --- a/lib/client/schema.ts +++ b/lib/client/schema.ts @@ -16,6 +16,7 @@ export interface ProfileData { contactInfo: string; intellectualInterests: { interest?: { name?: string, id?: string } }[]; coreValues: { value?: { name?: string, id?: string } }[]; + books: { value?: { name?: string, id?: string } }[]; causeAreas: { causeArea?: { name?: string, id?: string } }[]; desiredConnections: { connection?: { name?: string, id?: string } }[]; promptAnswers: { prompt?: string; answer?: string, id?: string }[]; @@ -23,7 +24,7 @@ export interface ProfileData { }; } -export type DropdownKey = 'interests' | 'causeAreas' | 'connections' | 'coreValues'; +export type DropdownKey = 'interests' | 'causeAreas' | 'connections' | 'coreValues' | 'books'; export type RangeKey = 'age' | 'introversion'; // type OtherKey = 'gender' | 'searchQuery'; diff --git a/lib/server/db-utils.ts b/lib/server/db-utils.ts index cfcf4b95..2f74287c 100644 --- a/lib/server/db-utils.ts +++ b/lib/server/db-utils.ts @@ -29,6 +29,7 @@ export async function retrieveUser(id: string) { intellectualInterests: {include: {interest: true}}, causeAreas: {include: {causeArea: true}}, coreValues: {include: {value: true}}, + books: {include: {value: true}}, desiredConnections: {include: {connection: true}}, promptAnswers: true, }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf90b611..c08fafa7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -42,6 +42,7 @@ model Profile { desiredConnections ProfileConnection[] intellectualInterests ProfileInterest[] coreValues ProfileValue[] + books ProfileBook[] promptAnswers PromptAnswer[] } @@ -63,6 +64,12 @@ model Value { users ProfileValue[] } +model Book { + id String @id @default(cuid()) + name String @unique + users ProfileBook[] +} + model CauseArea { id String @id @default(cuid()) name String @unique @@ -90,8 +97,17 @@ model ProfileInterest { model ProfileValue { profileId String valueId String - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) value Value @relation(fields: [valueId], references: [id], onDelete: Cascade) + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) + + @@id([profileId, valueId]) +} + +model ProfileBook { + profileId String + valueId String + value Book @relation(fields: [valueId], references: [id], onDelete: Cascade) + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@id([profileId, valueId]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index f767f599..2b0c16e4 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -16,6 +16,7 @@ async function main() { bio: string; interests: string[]; values: string[]; + books: string[]; }; const profiles: ProfileBio[] = [ @@ -27,7 +28,8 @@ async function main() { location: "Berlin, Germany", bio: "I’m passionate about understanding the limits and mechanics of human reasoning. I spend weekends dissecting papers on decision theory and evenings debating moral uncertainty. If you know your way around LessWrong and thought experiments, we’ll get along.", interests: ["Bayesian epistemology", "AI alignment", "Effective Altruism", "Meditation", "Game Theory"], - values: ["Intellectualism", "Rationality", "Autonomy"] + values: ["Intellectualism", "Rationality", "Autonomy"], + books: ["Daniel Kahneman - Thinking, Fast and Slow"] }, { name: "Marcus", @@ -37,7 +39,8 @@ async function main() { location: "San Francisco, USA", bio: "Practicing instrumental rationality one well-calibrated belief at a time. Stoicism and startup life have taught me a lot about tradeoffs. Looking for someone who can argue in good faith and loves truth-seeking as much as I do.", interests: ["Stoicism", "Predictive processing", "Rational fiction", "Startups", "Causal inference"], - values: ["Diplomacy", "Rationality", "Community"] + values: ["Diplomacy", "Rationality", "Community"], + books: ["Daniel Kahneman - Thinking, Fast and Slow"] }, { name: "Aya", @@ -47,7 +50,8 @@ async function main() { location: "Oxford, UK", bio: "My research focuses on metaethics and formal logic, but my heart belongs to moral philosophy. I think a lot about personhood, consciousness, and the ethics of future civilizations. Let's talk about Rawls or Parfit over tea.", interests: ["Metaethics", "Consciousness", "Transhumanism", "Moral realism", "Formal logic"], - values: ["Radical Honesty", "Structure", "Sufficiency"] + values: ["Radical Honesty", "Structure", "Sufficiency"], + books: ["Daniel Kahneman - Thinking, Fast and Slow"] }, { name: "David", @@ -57,7 +61,8 @@ async function main() { location: "Toronto, Canada", bio: "Former humanities major turned quant. Still fascinated by existential risk, the philosophy of science, and how to stay sane in an uncertain world. I'm here to meet people who think weird is a compliment.", interests: ["Probability theory", "Longtermism", "Epistemic humility", "Futurology", "Meditation"], - values: ["Conservatism", "Ambition", "Idealism"] + values: ["Conservatism", "Ambition", "Idealism"], + books: ["Daniel Kahneman - Thinking, Fast and Slow"] }, { name: "Mei", @@ -67,12 +72,14 @@ async function main() { location: "Singapore", bio: "Writing essays on intellectual humility, the philosophy of language, and how thinking styles shape our lives. I appreciate calm reasoning, rigorous curiosity, and the beauty of well-defined concepts. Let's try to model each other's minds.", interests: ["Philosophy of language", "Bayesian reasoning", "Writing", "Dialectics", "Systems thinking"], - values: ["Emotional Merging", "Sufficiency", "Pragmatism"] + values: ["Emotional Merging", "Sufficiency", "Pragmatism"], + books: ["Daniel Kahneman - Thinking, Fast and Slow"] } ]; const interests = new Set(); const values = new Set(); + const books = new Set(); profiles.forEach(profile => { profile.interests.forEach(v => interests.add(v)); @@ -82,8 +89,13 @@ async function main() { profile.values.forEach(v => values.add(v)); }); + profiles.forEach(profile => { + profile.books.forEach(v => books.add(v)); + }); + console.log('Interests:', [...interests]); console.log('Values:', [...values]); + console.log('Books:', [...books]); await prisma.interest.createMany({ data: [...interests].map(v => ({name: v})), @@ -95,6 +107,11 @@ async function main() { skipDuplicates: true, }); + await prisma.book.createMany({ + data: [...books].map(v => ({name: v})), + skipDuplicates: true, + }); + await prisma.causeArea.createMany({ data: [ {name: 'Climate Change'}, @@ -118,6 +135,7 @@ async function main() { // Get actual Interest & CauseArea objects const allInterests = await prisma.interest.findMany(); const allValues = await prisma.value.findMany(); + const allBooks = await prisma.book.findMany(); const allCauseAreas = await prisma.causeArea.findMany(); const allConnections = await prisma.connection.findMany(); @@ -155,6 +173,11 @@ async function main() { .filter(e => (new Set(profile.values)).has(e.name)) .map(e => ({valueId: e.id})) }, + books: { + create: allBooks + .filter(e => (new Set(profile.books)).has(e.name)) + .map(e => ({valueId: e.id})) + }, causeAreas: { create: [ {causeAreaId: allCauseAreas[i % allCauseAreas.length].id},