Add core values

This commit is contained in:
MartinBraquet
2025-08-02 19:22:03 +02:00
parent 18f8fc7cc5
commit 7205245d29
12 changed files with 498 additions and 461 deletions

View File

@@ -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(

View File

@@ -26,6 +26,11 @@ export async function GET() {
connection: true
},
},
coreValues: {
include: {
value: true
},
},
}
}
}

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<Set<string>>(new Set());
const [selectedConnections, setSelectedConnections] = useState<Set<string>>(new Set());
const [newInterest, setNewInterest] = useState('');
const [newConnection, setNewConnection] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownRefC = useRef<HTMLDivElement>(null);
const [showDropdown, setShowDropdown] = useState(false);
const [showDropdownC, setShowDropdownC] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(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<Set<string>>(new Set());
const dropdownRef = useRef<HTMLDivElement>(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<HTMLInputElement>) => {
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 <>
<div className="mt-2 mb-4">
<button
type="button"
onClick={() => setShowMoreInfo(!showMoreInfo)}
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
>
{showMoreInfo ? 'Hide info' : brief}
<svg
className={`w-4 h-4 ml-1 transition-transform ${showMoreInfo ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
</svg>
</button>
{showMoreInfo && (
<div className="mt-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md text-sm text-gray-700 dark:text-gray-300">
{text}
</div>
)}
</div>
</>;
}
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: <>
<p className="mt-2">
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 integrityeven when it's inconvenient. These
aren't traits you aspire to signal; theyre 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 truthfulavoid 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.
</p>
</>
},
{
id: 'interests', title: 'Core Interests', allowAdd: true,
content: <>
<p className="mt-2">
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 youre 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. Dont be afraid to show complexity or
contradiction; honesty invites resonance. The goal isnt to impress, but to be understoodby the
kind of person who wants to know you for who you actually are, not just what you do for fun.
</p>
</>
},
]
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 <>
<div className="relative" ref={dropdownRef}>
<label className={headingStyle}>
{title}
</label>
{content && getDetails(id, 'Guidance', content)}
<div className="relative">
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
<input
type="text"
value={newFeature}
maxLength={100}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowDropdown(!showDropdown)}
className="px-3 py-2 border-l border-gray-300 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"/>
</svg>
</button>
{allowAdd &&
<button
type="button"
onClick={addNewFeature}
disabled={!newFeature.trim()}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
}
</div>
{(showDropdown || newFeature) && (
<div
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-900 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{/* New interest option */}
{allowAdd && newFeature && !allFeatures.some(i =>
i.name.toLowerCase() === newFeature.toLowerCase()
) && (
<div
className=" cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
onClick={() => addNewFeature()}
>
<div className="flex items-center">
<span className="font-normal ml-3 block truncate">
Add "{newFeature}"
</span>
</div>
</div>
)}
{/* Filtered interests */}
{allFeatures
.filter(interest =>
interest.name.toLowerCase().includes(newFeature.toLowerCase())
)
.map((interest) => (
<div
key={interest.id}
className=" cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
onClick={() => {
toggleFeature(interest.id);
setNewFeature('');
}}
>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
checked={selectedFeatures.has(interest.id)}
onChange={(e) => {
e.stopPropagation()
}}
/>
<span className="font-normal ml-3 block truncate">{interest.name}</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Selected interests */}
<div className="flex flex-wrap gap-2 mt-3">
{Array.from(selectedFeatures).map(featureId => {
const interest = allFeatures.find(i => i.id === featureId);
if (!interest) return null;
return (
<span
key={featureId}
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
>
{interest.name}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleFeature(featureId);
}}
className="ml-1.5 inline-flex items-center justify-center h-4 w-4 rounded-full bg-blue-200 hover:bg-blue-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="sr-only">Remove {interest.name}</span>
<svg className="h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
<path
d="M4 3.293L6.646.646a.5.5 0 01.708.708L4.707 4l2.647 2.646a.5.5 0 01-.708.708L4 4.707l-2.646 2.647a.5.5 0 01-.708-.708L3.293 4 .646 1.354a.5.5 0 01.708-.708L4 3.293z"/>
</svg>
</button>
</span>
);
})}
</div>
</div>
</>
}
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
@@ -505,217 +684,9 @@ function RegisterComponent() {
/>
</div>
<div className="relative" ref={dropdownRefC}>
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-2">
Desired Connections
</label>
<div className="relative">
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
<input
type="text"
value={newConnection}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowDropdownC(!showDropdownC)}
className="px-3 py-2 border-l border-gray-300 text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"/>
</svg>
</button>
</div>
</div>
{(showDropdownC) && (<div
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-900 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black dark:ring-white ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{allConnections
.filter(v => v.name.toLowerCase().includes(newConnection.toLowerCase()))
.map((v) => (
<div
key={v.id}
className=" dark:text-white cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
onClick={() => {
toggleConnection(v.id);
setNewConnection('');
// setNewInterest('');
}}
>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
checked={selectedConnections.has(v.id)}
onChange={() => {
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="font-normal ml-3 block truncate">{v.name}</span>
</div>
</div>
))}
</div>
)
}
<div className="flex flex-wrap gap-2 mt-3">
{Array.from(selectedConnections).map(id => {
const v = allConnections.find(i => i.id === id);
if (!v) return null;
return (
<span
key={id}
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
>
{v.name}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleConnection(id);
}}
className="ml-1.5 inline-flex items-center justify-center h-4 w-4 rounded-full bg-blue-200 hover:bg-blue-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="sr-only">Remove {v.name}</span>
<svg className="h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
<path
d="M4 3.293L6.646.646a.5.5 0 01.708.708L4.707 4l2.647 2.646a.5.5 0 01-.708.708L4 4.707l-2.646 2.647a.5.5 0 01-.708-.708L3.293 4 .646 1.354a.5.5 0 01.708-.708L4 3.293z"/>
</svg>
</button>
</span>
);
})}
</div>
</div>
<div className="relative" ref={dropdownRef}>
<label className={headingStyle}>
Core Interests
</label>
<div className="relative">
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
<input
type="text"
value={newInterest}
maxLength={100}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowDropdown(!showDropdown)}
className="px-3 py-2 border-l border-gray-300 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"/>
</svg>
</button>
<button
type="button"
onClick={addNewInterest}
disabled={!newInterest.trim()}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
</div>
{(showDropdown || newInterest) && (
<div
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-900 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{/* New interest option */}
{newInterest && !allInterests.some(i =>
i.name.toLowerCase() === newInterest.toLowerCase()
) && (
<div
className=" cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
onClick={() => addNewInterest()}
>
<div className="flex items-center">
<span className="font-normal ml-3 block truncate">
Add "{newInterest}"
</span>
</div>
</div>
)}
{/* Filtered interests */}
{allInterests
.filter(interest =>
interest.name.toLowerCase().includes(newInterest.toLowerCase())
)
.map((interest) => (
<div
key={interest.id}
className=" cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
onClick={() => {
toggleInterest(interest.id);
setNewInterest('');
}}
>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
checked={selectedInterests.has(interest.id)}
onChange={() => {
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="font-normal ml-3 block truncate">
{interest.name}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Selected interests */}
<div className="flex flex-wrap gap-2 mt-3">
{Array.from(selectedInterests).map(interestId => {
const interest = allInterests.find(i => i.id === interestId);
if (!interest) return null;
return (
<span
key={interestId}
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
>
{interest.name}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleInterest(interestId);
}}
className="ml-1.5 inline-flex items-center justify-center h-4 w-4 rounded-full bg-blue-200 hover:bg-blue-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="sr-only">Remove {interest.name}</span>
<svg className="h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
<path
d="M4 3.293L6.646.646a.5.5 0 01.708.708L4.707 4l2.647 2.646a.5.5 0 01-.708.708L4 4.707l-2.646 2.647a.5.5 0 01-.708-.708L3.293 4 .646 1.354a.5.5 0 01.708-.708L4 3.293z"/>
</svg>
</button>
</span>
);
})}
</div>
</div>
{getDropdown(dropdownConfig[0])}
{getDropdown(dropdownConfig[1])}
{getDropdown(dropdownConfig[2])}
<div>
<label htmlFor="introversion" className={headingStyle}>
@@ -777,48 +748,46 @@ function RegisterComponent() {
<div className="max-w-3xl w-full">
<label htmlFor="description" className={headingStyle}>
About You
<p className="text-xs italic">
<p className="text-sm italic">
Feel free to include any relevant links (dating / friends docs, etc.), but consider copy-pasting
the content here so that people can find you by keyword search.
</p>
</label>
<div className="mt-2 mb-4">
<button
type="button"
onClick={() => setShowMoreInfo(!showMoreInfo)}
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
>
{showMoreInfo ? 'Hide info' : 'Key details for fulfilling encounters'}
<svg
className={`w-4 h-4 ml-1 transition-transform ${showMoreInfo ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showMoreInfo && (
<div className="mt-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md text-sm text-gray-700 dark:text-gray-300">
<p className="mt-2">To the extent that you are comfortable sharing, consider writing about:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Your interests and what you're looking for: type of connection, activities to do, etc.</li>
<li>Your availability and timezone</li>
<li>What makes you unique</li>
<li>Your expectations and boundaries</li>
<li>Your intellectual interests (currently exploring, favorite, and least favorite)</li>
<li>Your core values</li>
<li>Your altruistic values: community engagement, social justice, and other cause areas</li>
<li>Your level of education, hobbies, pets, habits, subcultures, diet, emotional sensitivity, sense of humor, ambition, organization, pet peeves, non-negotiables</li>
<li>Your thinking style, results from evidence-based personality tests (e.g., Big 5)</li>
<li>Your physical and mental health: some traits that rub people the wrong way, triggers, therapy, or what you are trying to improve</li>
<li>If interested in romantic relationships, your love languages (giving and receiving), timeline, romantic orientation, family projects, work-life balance, financial goals / habits, career goals, housing situation (renting vs owning), and whether you would date someone who already has kids</li>
<li>What you would like in your ideal person or connection—where they should be similar or different from your own description</li>
<li>Conversation starters or questions</li>
</ul>
</div>
)}
</div>
{getDetails(
'description',
'Key details for fulfilling encounters',
<>
<p className="mt-2">To the extent that you are comfortable sharing, consider writing about:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Your interests and what you're looking for: type of connection, activities to do, etc.</li>
<li>Your availability and timezone</li>
<li>What makes you unique</li>
<li>Your expectations and boundaries</li>
<li>Your intellectual interests (currently exploring, favorite, and least favorite)</li>
<li>Your core values</li>
<li>Your altruistic values: community engagement, social justice, and other cause areas</li>
<li>Your level of education, hobbies, pets, habits, subcultures, diet, emotional sensitivity, sense
of
humor, ambition, organization, pet peeves, non-negotiables
</li>
<li>Your thinking style, results from evidence-based personality tests (e.g., Big 5)</li>
<li>Your physical and mental health: some traits that rub people the wrong way, triggers, therapy,
or what
you are trying to improve
</li>
<li>If interested in romantic relationships, your love languages (giving and receiving), timeline,
romantic orientation, family projects, work-life balance, financial goals / habits, career goals,
housing situation (renting vs owning), and whether you would date someone who already has kids
</li>
<li>What you would like in your ideal person or connection—where they should be similar or different
from
your own description
</li>
<li>Conversation starters or questions</li>
</ul>
</>
)
}
<textarea
id="description"
name="description"

View File

@@ -11,6 +11,7 @@ interface FilterProps {
filters: {
gender: string;
interests: string[];
coreValues: string[];
connections: string[];
causeAreas: string[];
searchQuery: string;
@@ -28,6 +29,7 @@ interface FilterProps {
export const dropdownConfig: { id: DropdownKey, name: string }[] = [
{id: "connections", name: "Desired Connections"},
{id: "interests", name: "Core Interests"},
{id: "coreValues", name: "Core Values"},
{id: "causeAreas", name: "Cause Areas"},
]
@@ -69,6 +71,7 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
const data = await res.json() as Record<string, Item[]>;
console.log(data);
for (const [id, values] of Object.entries(data)) {
console.log(id)
dropDownStates[id].options.set(values);
}
}

View File

@@ -18,13 +18,14 @@ const initialState = {
minIntroversion: null as number | null,
maxIntroversion: null as number | null,
interests: [] as string[],
coreValues: [] as string[],
causeAreas: [] as string[],
connections: [] as string[],
searchQuery: '',
forceRun: false,
};
export type DropdownKey = 'interests' | 'causeAreas' | 'connections';
export type DropdownKey = 'interests' | 'causeAreas' | 'connections' | 'coreValues';
export type RangeKey = 'age' | 'introversion';
// type OtherKey = 'gender' | 'searchQuery';

View File

@@ -118,6 +118,7 @@ export function getProfile(url: string, header: any = null) {
const tagsConfig: Tags[] = [
{profileAttribute: 'desiredConnections', attribute: 'connection', title: 'Desired Connections'},
{profileAttribute: 'intellectualInterests', attribute: 'interest', title: 'Core Interests'},
{profileAttribute: 'coreValues', attribute: 'value', title: 'Core Values'},
{profileAttribute: 'causeAreas', attribute: 'causeArea', title: 'Cause Areas'},
]
@@ -293,7 +294,7 @@ export function getProfile(url: string, header: any = null) {
key={idx}
// className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
>
{value.prompt} {value.answer}
<i>{value.prompt}</i><br/>{value.answer}
</li>
))
}

View File

@@ -15,6 +15,7 @@ export interface ProfileData {
description: string;
contactInfo: string;
intellectualInterests: { interest?: { name?: string, id?: string } }[];
coreValues: { value?: { name?: string, id?: string } }[];
causeAreas: { causeArea?: { name?: string, id?: string } }[];
desiredConnections: { connection?: { name?: string, id?: string } }[];
promptAnswers: { prompt?: string; answer?: string, id?: string }[];

View File

@@ -45,9 +45,10 @@ model Profile {
// communicationPreferences CommunicationPreferences[]
// Many-to-many
promptAnswers PromptAnswer[]
promptAnswers PromptAnswer[]
desiredConnections ProfileConnection[]
intellectualInterests ProfileInterest[]
coreValues ProfileValue[]
causeAreas ProfileCauseArea[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -85,6 +86,12 @@ model Interest {
users ProfileInterest[]
}
model Value {
id String @id @default(cuid())
name String @unique
users ProfileValue[]
}
model CauseArea {
id String @id @default(cuid())
name String @unique
@@ -113,6 +120,16 @@ model ProfileInterest {
@@id([profileId, interestId])
}
model ProfileValue {
profileId String
valueId String
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
value Value @relation(fields: [valueId], references: [id], onDelete: Cascade)
@@id([profileId, valueId])
}
model ProfileCauseArea {
profileId String
causeAreaId String

View File

@@ -15,6 +15,7 @@ async function main() {
location: string;
bio: string;
interests: string[];
values: string[];
};
const profiles: ProfileBio[] = [
@@ -25,7 +26,8 @@ async function main() {
occupation: "Cognitive Science Researcher",
location: "Berlin, Germany",
bio: "Im 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, well get along.",
interests: ["Bayesian epistemology", "AI alignment", "Effective Altruism", "Meditation", "Game Theory"]
interests: ["Bayesian epistemology", "AI alignment", "Effective Altruism", "Meditation", "Game Theory"],
values: ["Intellectualism", "Rationality", "Autonomy"]
},
{
name: "Marcus",
@@ -34,7 +36,8 @@ async function main() {
occupation: "Software Engineer",
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"]
interests: ["Stoicism", "Predictive processing", "Rational fiction", "Startups", "Causal inference"],
values: ["Diplomacy", "Rationality", "Community"]
},
{
name: "Aya",
@@ -43,7 +46,8 @@ async function main() {
occupation: "Philosophy PhD Candidate",
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"]
interests: ["Metaethics", "Consciousness", "Transhumanism", "Moral realism", "Formal logic"],
values: ["Radical Honesty", "Structure", "Sufficiency"]
},
{
name: "David",
@@ -52,7 +56,8 @@ async function main() {
occupation: "Data Scientist",
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"]
interests: ["Probability theory", "Longtermism", "Epistemic humility", "Futurology", "Meditation"],
values: ["Conservatism", "Ambition", "Idealism"]
},
{
name: "Mei",
@@ -61,23 +66,32 @@ async function main() {
occupation: "Independent Writer",
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"]
interests: ["Philosophy of language", "Bayesian reasoning", "Writing", "Dialectics", "Systems thinking"],
values: ["Emotional Merging", "Sufficiency", "Pragmatism"]
}
];
const interests = new Set<string>();
const values = new Set<string>();
profiles.forEach(profile => {
profile.interests.forEach(interest => {
interests.add(interest);
});
profile.interests.forEach(v => interests.add(v));
});
profiles.forEach(profile => {
profile.values.forEach(v => values.add(v));
});
console.log('Interests:', [...interests]);
console.log('Values:', [...values]);
// Create some interests and cause areas
await prisma.interest.createMany({
data: [...interests].map(interest => ({name: interest})),
data: [...interests].map(v => ({name: v})),
skipDuplicates: true,
});
await prisma.value.createMany({
data: [...values].map(v => ({name: v})),
skipDuplicates: true,
});
@@ -102,6 +116,7 @@ async function main() {
// Get actual Interest & CauseArea objects
const allInterests = await prisma.interest.findMany();
const allValues = await prisma.value.findMany();
const allCauseAreas = await prisma.causeArea.findMany();
const allConnections = await prisma.connection.findMany();
@@ -130,10 +145,14 @@ async function main() {
],
},
intellectualInterests: {
// create: [...allInterests].map(interest => ({interestId: interest.id}) && Set(profile.interests))
create: allInterests
.filter(e => (new Set(profile.interests)).has(e.name)) // keep only elements in the set
.map(e => ({interestId: e.id})) // map to object with `a` key
.filter(e => (new Set(profile.interests)).has(e.name))
.map(e => ({interestId: e.id}))
},
coreValues: {
create: allValues
.filter(e => (new Set(profile.values)).has(e.name))
.map(e => ({valueId: e.id}))
},
causeAreas: {
create: [