From 7205245d29e49911a791cf92d455d8ec55e0785a Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sat, 2 Aug 2025 19:22:03 +0200 Subject: [PATCH] Add core values --- app/api/interests/route.ts | 13 +- app/api/profile/route.ts | 5 + app/api/profiles/[id]/route.ts | 1 + app/api/profiles/route.ts | 18 + app/api/user/update-profile/route.ts | 93 ++-- app/complete-profile/page.tsx | 755 +++++++++++++-------------- app/profiles/ProfileFilters.tsx | 3 + app/profiles/page.tsx | 3 +- lib/client/profile.tsx | 3 +- lib/client/schema.ts | 1 + prisma/schema.prisma | 19 +- prisma/seed.ts | 45 +- 12 files changed, 498 insertions(+), 461 deletions(-) diff --git a/app/api/interests/route.ts b/app/api/interests/route.ts index a9233da4..ab5f78ef 100644 --- a/app/api/interests/route.ts +++ b/app/api/interests/route.ts @@ -16,6 +16,17 @@ export async function GET() { cacheStrategy: cacheStrategy, }); + const coreValues = await prisma.value.findMany({ + select: { + id: true, + name: true, + }, + orderBy: { + name: 'asc' + }, + cacheStrategy: cacheStrategy, + }); + const causeAreas = await prisma.causeArea.findMany({ select: { id: true, @@ -38,7 +49,7 @@ export async function GET() { cacheStrategy: cacheStrategy, }); - return NextResponse.json({ interests, causeAreas, connections }); + return NextResponse.json({ interests, coreValues, causeAreas, connections }); } catch (error) { console.error('Error fetching interests:', error); return NextResponse.json( diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index d78ebe1b..af9c1f03 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -26,6 +26,11 @@ export async function GET() { connection: true }, }, + coreValues: { + include: { + value: true + }, + }, } } } diff --git a/app/api/profiles/[id]/route.ts b/app/api/profiles/[id]/route.ts index 8ec201eb..d539fa33 100644 --- a/app/api/profiles/[id]/route.ts +++ b/app/api/profiles/[id]/route.ts @@ -25,6 +25,7 @@ export async function GET( include: { intellectualInterests: { include: { interest: true } }, causeAreas: { include: { causeArea: true } }, + coreValues: { include: { value: true } }, desiredConnections: { include: { connection: true } }, promptAnswers: true, }, diff --git a/app/api/profiles/route.ts b/app/api/profiles/route.ts index 78d32f27..f7b15dda 100644 --- a/app/api/profiles/route.ts +++ b/app/api/profiles/route.ts @@ -11,6 +11,7 @@ export async function GET(request: Request) { const minIntroversion = url.searchParams.get("minIntroversion"); 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 causeAreas = url.searchParams.get("causeAreas")?.split(",").filter(Boolean) || []; const connections = url.searchParams.get("connections")?.split(",").filter(Boolean) || []; const searchQuery = url.searchParams.get("search") || ""; @@ -96,6 +97,22 @@ export async function GET(request: Request) { }; } + // AND + if (coreValues.length > 0) { + where.profile = { + ...where.profile, + AND: coreValues.map((name) => ({ + coreValues: { + some: { + value: { + name: name, + }, + }, + }, + })), + }; + } + // OR if (connections.length > 0) { where.profile = { @@ -179,6 +196,7 @@ export async function GET(request: Request) { profile: { include: { intellectualInterests: {include: {interest: true}}, + coreValues: {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 75fcdd65..8d3feee3 100644 --- a/app/api/user/update-profile/route.ts +++ b/app/api/user/update-profile/route.ts @@ -14,7 +14,7 @@ export async function POST(req: Request) { } const data = await req.json(); - const {profile, image, name, interests = [], connections = []} = data; + const {profile, image, name, interests = [], connections = [], coreValues = []} = data; Object.keys(profile).forEach(key => { if (profile[key] === '' || !profile[key]) { @@ -44,65 +44,56 @@ export async function POST(req: Request) { }, }); - // Process interests if any - if (interests.length > 0 && updatedUser.profile) { - // First, find or create all interests - const interestOperations = interests.map((interest: { id?: string; name: string }) => - prisma.interest.upsert({ - where: {id: interest.id || ''}, - update: {name: interest.name}, - create: {name: interest.name}, - }) - ); + const modelMap = { + interest: prisma.interest, + profileInterest: prisma.profileInterest, + connection: prisma.connection, + profileConnection: prisma.profileConnection, + value: prisma.value, + profileValue: prisma.profileValue, + } as const; - const createdInterests = await Promise.all(interestOperations); + async function handleFeatures(features, attribute: string, profileAttribute: string, idName: string) { + // Process interests if any + if (features.length > 0 && updatedUser.profile) { + // First, find or create all features + console.log('profile', profileAttribute, profileAttribute); + const operations = features.map((feat: { id?: string; name: string }) => + modelMap[attribute].upsert({ + where: {id: feat.id || ''}, + update: {name: feat.name}, + create: {name: feat.name}, + }) + ); - // Get the IDs of all created/updated interests - const interestIds = createdInterests.map(interest => interest.id); + const createdFeatures = await Promise.all(operations); - // First, remove all existing interests for this profile - await prisma.profileInterest.deleteMany({ - where: {profileId: updatedUser.profile.id}, - }); + // Get the IDs of all created/updated features + const ids = createdFeatures.map(v => v.id); - // Then, create new connections - if (interestIds.length > 0) { - await prisma.profileInterest.createMany({ - data: interestIds.map(interestId => ({ - profileId: updatedUser.profile!.id, - interestId, - })), - skipDuplicates: true, + // First, remove all existing interests for this profile + await modelMap[profileAttribute].deleteMany({ + where: {profileId: updatedUser.profile.id}, }); + + // Then, create new connections + if (ids.length > 0) { + await modelMap[profileAttribute].createMany({ + data: ids.map(id => ({ + profileId: updatedUser.profile!.id, + [idName]: id, + })), + skipDuplicates: true, + }); + } } } - if (connections.length > 0 && updatedUser.profile) { - // First, find or create all interests - const connectionOperations = connections.map((v: { id?: string; name: string }) => - prisma.connection.upsert({ - where: {id: v.id || ''}, - update: {name: v.name}, - create: {name: v.name}, - }) - ); - const createdConnections = await Promise.all(connectionOperations); - const connectionIds = createdConnections.map(v => v.id); - await prisma.profileConnection.deleteMany({ - where: {profileId: updatedUser.profile.id}, - }); - if (connectionIds.length > 0) { - await prisma.profileConnection.createMany({ - data: connectionIds.map(id => ({ - profileId: updatedUser.profile!.id, - connectionId: id, - })), - skipDuplicates: true, - }); - } - } + await handleFeatures(interests, 'interest', 'profileInterest', 'interestId') + await handleFeatures(connections, 'connection', 'profileConnection', 'connectionId') + await handleFeatures(coreValues, 'value', 'profileValue', 'valueId') - return updatedUser; + return updatedUser }); return NextResponse.json(result); diff --git a/app/complete-profile/page.tsx b/app/complete-profile/page.tsx index a20c183e..e3098c91 100644 --- a/app/complete-profile/page.tsx +++ b/app/complete-profile/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import {ChangeEvent, Suspense, useEffect, useRef, useState} from 'react'; +import {ChangeEvent, ReactNode, Suspense, useEffect, useRef, useState} from 'react'; import {useRouter, useSearchParams} from 'next/navigation'; import {signOut, useSession} from 'next-auth/react'; import Image from 'next/image'; @@ -36,21 +36,33 @@ function RegisterComponent() { const [isSubmitting, setIsSubmitting] = useState(false); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); - const [allInterests, setAllInterests] = useState<{ id: string, name: string }[]>([]); - const [allConnections, setAllConnections] = useState<{ id: string, name: string }[]>([]); - const [selectedInterests, setSelectedInterests] = useState>(new Set()); - const [selectedConnections, setSelectedConnections] = useState>(new Set()); - const [newInterest, setNewInterest] = useState(''); - const [newConnection, setNewConnection] = useState(''); - const dropdownRef = useRef(null); - const dropdownRefC = useRef(null); - const [showDropdown, setShowDropdown] = useState(false); - const [showDropdownC, setShowDropdownC] = useState(false); const [isLoading, setIsLoading] = useState(true); const fileInputRef = useRef(null); const router = useRouter(); const {data: session, update} = useSession(); - const [showMoreInfo, setShowMoreInfo] = useState(false); + + const hooks = Object.fromEntries(['interests', 'coreValues', 'description', 'connections'].map((id) => { + const [showMoreInfo, setShowMoreInfo] = useState(false); + const [newFeature, setNewFeature] = useState(''); + const [allFeatures, setAllFeatures] = useState<{ id: string, name: string }[]>([]); + const [selectedFeatures, setSelectedFeatures] = useState>(new Set()); + const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + + return [id, { + showMoreInfo, + setShowMoreInfo, + newFeature, + setNewFeature, + allFeatures, + setAllFeatures, + selectedFeatures, + setSelectedFeatures, + dropdownRef, + showDropdown, + setShowDropdown + }] + })); const id = session?.user.id @@ -81,17 +93,17 @@ function RegisterComponent() { } // Set selected interests if any - if (profile.intellectualInterests?.length > 0) { - const ids = profile.intellectualInterests - .map((pi: any) => pi.interest.id); - setSelectedInterests(new Set(ids)); + function setSelectedFeatures(id: string, attribute: string, subAttribute: string) { + const feature = profile[attribute]; + if (feature?.length > 0) { + const ids = feature.map((pi: any) => pi[subAttribute].id); + hooks[id].setSelectedFeatures(new Set(ids)); + } } - if (profile.desiredConnections?.length > 0) { - const ids = profile.desiredConnections - .map((pi: any) => pi.connection.id); - setSelectedConnections(new Set(ids)); - } + setSelectedFeatures('interests', 'intellectualInterests', 'interest') + setSelectedFeatures('coreValues', 'coreValues', 'value') + setSelectedFeatures('connections', 'desiredConnections', 'connection') setImages([]) setKeys(profile?.images) @@ -113,80 +125,37 @@ function RegisterComponent() { }, [session]); // Load existing interests and set up click-outside handler - useEffect(() => { - async function fetchInterests() { - try { - const res = await fetch('/api/interests'); - if (res.ok) { - const data = await res.json(); - setAllInterests(data.interests || []); + for (const id of ['interests', 'coreValues', 'connections']) { + useEffect(() => { + async function fetchFeatures() { + try { + const res = await fetch('/api/interests'); + if (res.ok) { + const data = await res.json(); + hooks[id].setAllFeatures(data[id] || []); + } + } catch (error) { + console.error('Error loading' + id, error); } - } catch (error) { - console.error('Error loading interests:', error); } - } - fetchInterests(); + fetchFeatures(); - // Close dropdown when clicking outside - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setShowDropdown(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - // Load existing connections - useEffect(() => { - async function fetchConnections() { - try { - const res = await fetch('/api/interests'); - if (res.ok) { - const data = await res.json(); - setAllConnections(data.connections || []); + // Close dropdown when clicking outside + const handleClickOutside = (event: MouseEvent) => { + const hook = hooks[id]; + const current = hook.dropdownRef.current; + if (current && !current.contains(event.target as Node)) { + hook.setShowDropdown(false); } - } catch (error) { - console.error('Error loading interests:', error); - } - } + }; - fetchConnections(); - - // Close dropdown when clicking outside - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRefC.current && !dropdownRefC.current.contains(event.target as Node)) { - setShowDropdownC(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const toggleConnection = (id: string) => { - setSelectedConnections(prev => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return newSet; - }); - }; - - const handleKeyDownC = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setShowDropdownC(false); - } - }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + } if (isLoading) { return ( @@ -196,51 +165,6 @@ function RegisterComponent() { ); } - const toggleInterest = (interestId: string) => { - setSelectedInterests(prev => { - const newSet = new Set(prev); - if (newSet.has(interestId)) { - newSet.delete(interestId); - } else { - newSet.add(interestId); - } - return newSet; - }); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - addNewInterest(); - } else if (e.key === 'Escape') { - setShowDropdown(false); - } - }; - - const addNewInterest = (e?: React.FormEvent) => { - if (e) e.preventDefault(); - const interestToAdd = newInterest.trim(); - if (!interestToAdd) return; - - // Check if interest already exists (case-insensitive) - const existingInterest = allInterests.find( - i => i.name.toLowerCase() === interestToAdd.toLowerCase() - ); - - if (existingInterest) { - // Toggle selection if it exists - toggleInterest(existingInterest.id); - } else { - // Add new interest - const newInterestObj = {id: `new-${Date.now()}`, name: interestToAdd}; - setAllInterests(prev => [...prev, newInterestObj]); - setSelectedInterests(prev => new Set(prev).add(newInterestObj.id)); - } - - setNewInterest(''); - setShowDropdown(false); - }; - const handleImagesUpload = async (e: ChangeEvent) => { return handleImageUpload(e, false); } @@ -322,18 +246,15 @@ function RegisterComponent() { conflictStyle: conflictStyle as ConflictStyle, images: keys, }, - allConnections, - interests: Array.from(selectedInterests).map(id => ({ - id: id.startsWith('new-') ? undefined : id, - name: allInterests.find(i => i.id === id)?.name || id.replace('new-', '') - })), - connections: Array.from(selectedConnections).map(id => ({ - id: id, - name: allConnections.find(i => i.id === id)?.name || id - })), ...(key && {image: key}), ...(name && {name}), }; + for (const name of ['interests', 'connections', 'coreValues']) { + data[name] = Array.from(hooks[name].selectedFeatures).map(id => ({ + id: id.startsWith('new-') ? undefined : id, + name: hooks[name].allFeatures.find(i => i.id === id)?.name || id.replace('new-', '') + })); + } console.log('data', data) const response = await fetch('/api/user/update-profile', { method: 'POST', @@ -363,7 +284,265 @@ function RegisterComponent() { // const personalityOptions = Object.values(PersonalityType); const conflictOptions = Object.values(ConflictStyle); - const headingStyle = "block text-sm font-medium text-gray-700 dark:text-white mb-1"; + const headingStyle = "block text-base font-medium text-gray-700 dark:text-white mb-1"; + + function getDetails(id: string, brief: string, text: ReactNode) { + const hook = hooks[id]; + const showMoreInfo = hook.showMoreInfo; + const setShowMoreInfo = hook.setShowMoreInfo; + return <> +
+ + {showMoreInfo && ( +
+ {text} +
+ )} +
+ ; + } + + interface DropdownConfig { + id: string; + title: string; + allowAdd: boolean; + content: ReactNode; + } + + const dropdownConfig: DropdownConfig[] = [ + { + id: 'connections', title: 'Desired Connections', allowAdd: false, + content: null + }, + { + id: 'coreValues', title: 'Core Values', allowAdd: true, + content: <> +

+ When defining your core values on a platform meant for forming deep, lasting bonds, focus on what governs your + choices, shapes your relationships, and anchors your sense of integrity—even when it's inconvenient. These + aren't traits you aspire to signal; they’re principles you consistently return to when life is uncertain or + difficult. Think in terms of how you treat others (e.g. intellectual honesty, compassion, loyalty), how you + approach truth (e.g. humility, curiosity, critical thinking), and how you handle conflict or complexity (e.g. + courage, nuance, responsibility). Be specific and truthful—avoid vague terms like “kindness” unless you can + explain what it actually looks like in practice. The point isn't to be agreeable to everyone, but to be + legible to those who share or deeply respect the values that define you. That clarity is what builds trust—and + trust is the foundation of any bond worth keeping. +

+ + }, + { + id: 'interests', title: 'Core Interests', allowAdd: true, + content: <> +

+ When selecting your core interests on a platform designed to foster deep, lasting + bonds, think beyond surface-level hobbies and focus on what truly shapes how you see the world and + connect with others. Choose interests that reveal how you think, what you care about deeply, and + where you’re most engaged—intellectually, emotionally, or ethically. These might include long-term + fascinations (like moral philosophy, storytelling, or systems thinking), enduring questions you + wrestle with, or areas where you're actively growing. Don’t be afraid to show complexity or + contradiction; honesty invites resonance. The goal isn’t to impress, but to be understood—by the + kind of person who wants to know you for who you actually are, not just what you do for fun. +

+ + }, + ] + + function getDropdown({id, title, allowAdd, content}: DropdownConfig) { + const hook = hooks[id]; + const newFeature = hook.newFeature; + const setNewFeature = hook.setNewFeature; + const showDropdown = hook.showDropdown; + const setShowDropdown = hook.setShowDropdown; + const allFeatures = hook.allFeatures; + const setSelectedFeatures = hook.setSelectedFeatures; + const setAllFeatures = hook.setAllFeatures; + const dropdownRef = hook.dropdownRef; + const selectedFeatures = hook.selectedFeatures; + + const toggleFeature = (featureId: string) => { + setSelectedFeatures(prev => { + const newSet = new Set(prev); + if (newSet.has(featureId)) { + newSet.delete(featureId); + } else { + newSet.add(featureId); + } + return newSet; + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addNewFeature(); + } else if (e.key === 'Escape') { + setShowDropdown(false); + } + }; + + const addNewFeature = (e?: React.FormEvent) => { + if (e) e.preventDefault(); + const toAdd = newFeature.trim(); + if (!toAdd) return; + + // Check if interest already exists (case-insensitive) + const existingFeature = allFeatures.find( + i => i.name.toLowerCase() === toAdd.toLowerCase() + ); + + if (existingFeature) { + // Toggle selection if it exists + toggleFeature(existingFeature.id); + } else { + // Add new feature + const newObj = {id: `new-${Date.now()}`, name: toAdd}; + setAllFeatures(prev => [...prev, newObj]); + setSelectedFeatures(prev => new Set(prev).add(newObj.id)); + } + + setNewFeature(''); + setShowDropdown(false); + }; + + return <> +
+ + {content && getDetails(id, 'Guidance', content)} + +
+
+ setNewFeature(e.target.value)} + onFocus={() => setShowDropdown(true)} + onKeyDown={handleKeyDown} + className="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-md border-0 focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Type to search" + /> + + {allowAdd && + + } +
+ + {(showDropdown || newFeature) && ( +
+ {/* New interest option */} + {allowAdd && newFeature && !allFeatures.some(i => + i.name.toLowerCase() === newFeature.toLowerCase() + ) && ( +
addNewFeature()} + > +
+ + Add "{newFeature}" + +
+
+ )} + + {/* Filtered interests */} + {allFeatures + .filter(interest => + interest.name.toLowerCase().includes(newFeature.toLowerCase()) + ) + .map((interest) => ( +
{ + toggleFeature(interest.id); + setNewFeature(''); + }} + > +
+ { + e.stopPropagation() + }} + /> + {interest.name} +
+
+ ))} +
+ )} +
+ + {/* Selected interests */} +
+ {Array.from(selectedFeatures).map(featureId => { + const interest = allFeatures.find(i => i.id === featureId); + if (!interest) return null; + return ( + + {interest.name} + + + ); + })} +
+
+ + } return (
@@ -505,217 +684,9 @@ function RegisterComponent() { />
-
- -
-
- setNewConnection(e.target.value)} - onFocus={() => setShowDropdownC(true)} - onKeyDown={handleKeyDownC} - className="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-md border-0 focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Type to search" - /> - -
-
- - {(showDropdownC) && (
- {allConnections - .filter(v => v.name.toLowerCase().includes(newConnection.toLowerCase())) - .map((v) => ( -
{ - toggleConnection(v.id); - setNewConnection(''); - // setNewInterest(''); - }} - > -
- { - }} - onClick={(e) => e.stopPropagation()} - /> - {v.name} -
-
- ))} -
- ) - } -
- {Array.from(selectedConnections).map(id => { - const v = allConnections.find(i => i.id === id); - if (!v) return null; - return ( - - {v.name} - - - ); - })} -
-
- -
- - -
-
- setNewInterest(e.target.value)} - onFocus={() => setShowDropdown(true)} - onKeyDown={handleKeyDown} - className="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-md border-0 focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Type to search or add new interest" - /> - - -
- - {(showDropdown || newInterest) && ( -
- {/* New interest option */} - {newInterest && !allInterests.some(i => - i.name.toLowerCase() === newInterest.toLowerCase() - ) && ( -
addNewInterest()} - > -
- - Add "{newInterest}" - -
-
- )} - - {/* Filtered interests */} - {allInterests - .filter(interest => - interest.name.toLowerCase().includes(newInterest.toLowerCase()) - ) - .map((interest) => ( -
{ - toggleInterest(interest.id); - setNewInterest(''); - }} - > -
- { - }} - onClick={(e) => e.stopPropagation()} - /> - - {interest.name} - -
-
- ))} -
- )} -
- - {/* Selected interests */} -
- {Array.from(selectedInterests).map(interestId => { - const interest = allInterests.find(i => i.id === interestId); - if (!interest) return null; - return ( - - {interest.name} - - - ); - })} -
-
+ {getDropdown(dropdownConfig[0])} + {getDropdown(dropdownConfig[1])} + {getDropdown(dropdownConfig[2])}