diff --git a/app/api/profiles/[id]/route.ts b/app/api/profiles/[id]/route.ts index be501259..5311037d 100644 --- a/app/api/profiles/[id]/route.ts +++ b/app/api/profiles/[id]/route.ts @@ -15,6 +15,7 @@ export async function GET( id: true, name: true, email: true, + image: true, createdAt: true, profile: { include: { diff --git a/app/api/profiles/route.ts b/app/api/profiles/route.ts index 582aead6..95bc20e7 100644 --- a/app/api/profiles/route.ts +++ b/app/api/profiles/route.ts @@ -1,13 +1,6 @@ -import { prisma } from "@/lib/server/prisma"; -import { NextResponse } from "next/server"; -import { getSession } from "@/lib/server/auth"; - -type FilterParams = { - gender?: string; - interests?: string[]; - causeAreas?: string[]; - searchQuery?: string; -}; +import {prisma} from "@/lib/server/prisma"; +import {NextResponse} from "next/server"; +import {getSession} from "@/lib/server/auth"; export async function GET(request: Request) { const url = new URL(request.url); @@ -16,7 +9,7 @@ export async function GET(request: Request) { const interests = url.searchParams.get("interests")?.split(",").filter(Boolean) || []; const causeAreas = url.searchParams.get("causeAreas")?.split(",").filter(Boolean) || []; const searchQuery = url.searchParams.get("search") || ""; - + const profilesPerPage = 20; const offset = (page - 1) * profilesPerPage; @@ -25,7 +18,7 @@ export async function GET(request: Request) { // Build the where clause based on filters const where: any = { - id: { not: session?.user?.id }, + id: {not: session?.user?.id}, }; if (gender) { @@ -35,16 +28,33 @@ export async function GET(request: Request) { }; } + // OR + // if (interests.length > 0) { + // where.profile = { + // ...where.profile, + // intellectualInterests: { + // some: { + // interest: { + // name: {in: interests}, + // }, + // }, + // }, + // }; + // } + + // AND if (interests.length > 0) { where.profile = { ...where.profile, - intellectualInterests: { - some: { - interest: { - name: { in: interests }, + AND: interests.map((interestName) => ({ + intellectualInterests: { + some: { + interest: { + name: interestName, + }, }, }, - }, + })), }; } @@ -54,7 +64,7 @@ export async function GET(request: Request) { causeAreas: { some: { causeArea: { - name: { in: causeAreas }, + name: {in: causeAreas}, }, }, }, @@ -63,11 +73,11 @@ export async function GET(request: Request) { if (searchQuery) { where.OR = [ - { name: { contains: searchQuery, mode: 'insensitive' } }, - { email: { contains: searchQuery, mode: 'insensitive' } }, + {name: {contains: searchQuery, mode: 'insensitive'}}, + {email: {contains: searchQuery, mode: 'insensitive'}}, { profile: { - description: { contains: searchQuery, mode: 'insensitive' }, + description: {contains: searchQuery, mode: 'insensitive'}, }, }, ]; @@ -77,18 +87,19 @@ export async function GET(request: Request) { const profiles = await prisma.user.findMany({ skip: offset, take: profilesPerPage, - orderBy: { createdAt: "desc" }, + orderBy: {createdAt: "desc"}, where, select: { id: true, name: true, email: true, + image: true, createdAt: true, profile: { include: { - intellectualInterests: { include: { interest: true } }, - causeAreas: { include: { causeArea: true } }, - desiredConnections: { include: { connection: true } }, + intellectualInterests: {include: {interest: true}}, + causeAreas: {include: {causeArea: true}}, + desiredConnections: {include: {connection: true}}, promptAnswers: true, }, }, @@ -98,6 +109,6 @@ export async function GET(request: Request) { const totalProfiles = await prisma.user.count(); const totalPages = Math.ceil(totalProfiles / profilesPerPage); - console.log({ profiles, totalPages }); - return NextResponse.json({ profiles, totalPages }); + console.log({profiles, totalPages}); + return NextResponse.json({profiles, totalPages}); } diff --git a/app/api/user/update-profile/route.ts b/app/api/user/update-profile/route.ts index ff23320b..8248bb3f 100644 --- a/app/api/user/update-profile/route.ts +++ b/app/api/user/update-profile/route.ts @@ -16,6 +16,14 @@ export async function POST(req: Request) { const data = await req.json(); const { profile, image, interests = [] } = data; + Object.keys(profile).forEach(key => { + if (profile[key] === '' || !profile[key]) { + delete profile[key]; + } + }); + + console.log('profile', profile); + // Start a transaction to ensure data consistency const result = await prisma.$transaction(async (prisma) => { // First, update/create the profile diff --git a/app/complete-profile/page.tsx b/app/complete-profile/page.tsx index 75f60078..10c72afd 100644 --- a/app/complete-profile/page.tsx +++ b/app/complete-profile/page.tsx @@ -1,12 +1,16 @@ 'use client'; import {ChangeEvent, useEffect, useRef, useState} from 'react'; -import {useRouter} from 'next/navigation'; +import {useRouter, useSearchParams} from 'next/navigation'; import {useSession} from 'next-auth/react'; import Image from 'next/image'; import {ConflictStyle, Gender, PersonalityType} from "@prisma/client"; +import {parseImage} from "@/lib/client/media"; export default function CompleteProfile() { + const searchParams = useSearchParams(); + const redirect = searchParams.get('redirect') || '/'; + const [description, setDescription] = useState(''); const [contactInfo, setContactInfo] = useState(''); const [location, setLocation] = useState(''); @@ -18,15 +22,58 @@ export default function CompleteProfile() { const [isSubmitting, setIsSubmitting] = useState(false); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); - const [allInterests, setAllInterests] = useState<{id: string, name: string}[]>([]); + const [allInterests, setAllInterests] = useState<{ id: string, name: string }[]>([]); const [selectedInterests, setSelectedInterests] = useState>(new Set()); const [newInterest, setNewInterest] = useState(''); - const [showDropdown, setShowDropdown] = useState(false); const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + const [isLoading, setIsLoading] = useState(true); const fileInputRef = useRef(null); const router = useRouter(); const {data: session, update} = useSession(); + console.log('image', image) + + // Fetch user profile data + useEffect(() => { + async function fetchUserProfile() { + if (!session?.user?.email) return; + + try { + const response = await fetch('/api/profile'); + if (response.ok) { + const userData = await response.json(); + if (userData?.profile) { + const {profile} = userData; + setDescription(profile.description || ''); + setContactInfo(profile.contactInfo || ''); + setLocation(profile.location || ''); + setGender(profile.gender || ''); + setPersonalityType(profile.personalityType || null); + setConflictStyle(profile.conflictStyle || ''); + await parseImage(profile.image, setImage); + + // Set selected interests if any + if (profile.intellectualInterests?.length > 0) { + const interestIds = profile.intellectualInterests + .map((pi: any) => pi.interest.id); + setSelectedInterests(new Set(interestIds)); + } + } + if (userData?.image) { + await parseImage(userData.image, setImage); + } + } + } catch (error) { + console.error('Error fetching user profile:', error); + } finally { + setIsLoading(false); + } + } + + fetchUserProfile(); + }, [session]); + // Load existing interests and set up click-outside handler useEffect(() => { async function fetchInterests() { @@ -40,6 +87,7 @@ export default function CompleteProfile() { console.error('Error loading interests:', error); } } + fetchInterests(); // Close dropdown when clicking outside @@ -55,6 +103,14 @@ export default function CompleteProfile() { }; }, []); + if (isLoading) { + return ( +
+
+
+ ); + } + const handleImageUpload = async (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -123,7 +179,7 @@ export default function CompleteProfile() { 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() @@ -134,11 +190,11 @@ export default function CompleteProfile() { toggleInterest(existingInterest.id); } else { // Add new interest - const newInterestObj = { id: `new-${Date.now()}`, name: interestToAdd }; + const newInterestObj = {id: `new-${Date.now()}`, name: interestToAdd}; setAllInterests(prev => [...prev, newInterestObj]); setSelectedInterests(prev => new Set(prev).add(newInterestObj.id)); } - + setNewInterest(''); setShowDropdown(false); }; @@ -187,7 +243,7 @@ export default function CompleteProfile() { } await update(); - router.push('/'); + router.push(redirect); } catch (error) { console.error('Profile update error:', error); setError(error instanceof Error ? error.message : 'Failed to update profile'); @@ -196,13 +252,6 @@ export default function CompleteProfile() { } }; - useEffect(() => { - const img = session?.user?.image; - if (img) { - setImage(img); - } - }, [session]); - const genderOptions = Object.values(Gender); const personalityOptions = Object.values(PersonalityType); const conflictOptions = Object.values(ConflictStyle); @@ -284,7 +333,7 @@ export default function CompleteProfile() { id="gender" name="gender" required - value={gender} + value={gender || ''} onChange={(e) => setGender(e.target.value as Gender)} className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" > @@ -319,7 +368,7 @@ export default function CompleteProfile() { setConflictStyle(e.target.value as ConflictStyle)} className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" > @@ -356,7 +405,7 @@ export default function CompleteProfile() { - +
setShowDropdown(!showDropdown)} className="px-3 py-2 border-l border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" > - - + +
+
{/* New interest option */} - {newInterest && !allInterests.some(i => + {newInterest && !allInterests.some(i => i.name.toLowerCase() === newInterest.toLowerCase() ) && ( -
addNewInterest()} > @@ -407,7 +460,7 @@ export default function CompleteProfile() { {/* Filtered interests */} {allInterests - .filter(interest => + .filter(interest => interest.name.toLowerCase().includes(newInterest.toLowerCase()) ) .map((interest) => ( @@ -424,7 +477,8 @@ export default function CompleteProfile() { type="checkbox" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" checked={selectedInterests.has(interest.id)} - onChange={() => {}} + onChange={() => { + }} onClick={(e) => e.stopPropagation()} /> @@ -436,7 +490,7 @@ export default function CompleteProfile() {
)}
- + {/* Selected interests */}
{Array.from(selectedInterests).map(interestId => { @@ -458,7 +512,8 @@ export default function CompleteProfile() { > Remove {interest.name} - + diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 1666ed41..d406c3b7 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -4,34 +4,43 @@ import Link from 'next/link'; import Image from 'next/image'; import {useEffect, useState} from "react"; import LoadingSpinner from "@/lib/client/LoadingSpinner"; +import {parseImage} from "@/lib/client/media"; +import {usePathname} from "next/navigation"; export default function ProfilePage() { + const pathname = usePathname(); // Get the current route const [userData, setUserData] = useState(null); + const [image, setImage] = useState(null); try { useEffect(() => { - fetch('/api/profile').then(r => r.json()).then(data => { + async function fetchImage() { + const res = await fetch('/api/profile'); + const data = await res.json(); setUserData(data); console.log('userData', data); - }); + if (data?.image) { + await parseImage(data.image, setImage); + } + } + + fetchImage(); }, []); if (!userData) { - return ; + return ; } - const { - name, - email, - image, - profile: { - location, - description, - gender, - personalityType, - conflictStyle, - intellectualInterests - } - } = userData; + console.log('userData', userData); + const name = userData?.name ?? ''; + const email = userData?.email ?? ''; + + const profile = userData?.profile; + const location = profile?.location ?? ''; + const description = profile?.description ?? ''; + const gender = profile?.gender ?? ''; + const personalityType = profile?.personalityType ?? ''; + const conflictStyle = profile?.conflictStyle ?? ''; + const intellectualInterests = profile?.intellectualInterests ?? []; return (
@@ -42,7 +51,7 @@ export default function ProfilePage() {

View and update your profile information

Edit Profile diff --git a/app/profiles/ProfileFilters.tsx b/app/profiles/ProfileFilters.tsx index 84b89fea..3b783dd7 100644 --- a/app/profiles/ProfileFilters.tsx +++ b/app/profiles/ProfileFilters.tsx @@ -1,34 +1,7 @@ 'use client'; -import { useState } from 'react'; - -// Mock data for filter options -const GENDER_OPTIONS = [ - { value: 'Male', label: 'Male' }, - { value: 'Female', label: 'Female' }, - { value: 'NonBinary', label: 'Non-binary' }, - { value: 'Other', label: 'Prefer not to say' }, -]; - -// These would ideally come from your database -const INTEREST_OPTIONS = [ - 'AI Safety', - 'Philosophy', - 'Effective Altruism', - 'Rationality', - 'Technology', - 'Science', - 'Policy', -]; - -const CAUSE_AREA_OPTIONS = [ - 'AI Safety', - 'Global Health', - 'Animal Welfare', - 'Biosecurity', - 'Climate Change', - 'Global Poverty', -]; +import {useEffect, useRef, useState} from 'react'; +import {Gender} from "@prisma/client"; interface FilterProps { filters: { @@ -42,8 +15,66 @@ interface FilterProps { onReset: () => void; } -export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onReset }: FilterProps) { - const [showFilters, setShowFilters] = useState(false); +export function ProfileFilters({filters, onFilterChange, onToggleFilter, onReset}: FilterProps) { + const [showFilters, setShowFilters] = useState(true); + const [allCauseAreas, setAllCauseAreas] = useState<{ id: string, name: string }[]>([]); + const [allInterests, setAllInterests] = useState<{ id: string, name: string }[]>([]); + const [selectedInterests, setSelectedInterests] = useState>(new Set()); + const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + const [newInterest, setNewInterest] = useState(''); + + useEffect(() => { + async function fetchInterests() { + try { + const res = await fetch('/api/interests'); + if (res.ok) { + const data = await res.json(); + setAllInterests(data.interests || []); + setAllCauseAreas(data.causeAreas || []); + console.log('All interests:', data.interests); + console.log('All cause areas:', data.causeAreas); + console.log('Gender', Gender); + } + } catch (error) { + console.error('Error loading interests:', error); + } + } + + fetchInterests(); + + // 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); + }; + }, []); + + + 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 === 'Escape') { + setShowDropdown(false); + } + }; + return (
@@ -57,18 +88,22 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese onChange={(e) => onFilterChange('searchQuery', e.target.value)} />
- - + +
- + @@ -85,47 +120,129 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese onChange={(e) => onFilterChange('gender', e.target.value)} > - {GENDER_OPTIONS.map((option) => ( - ))}
-
- -
- {INTEREST_OPTIONS.map((interest) => ( +
+ + +
+
+ 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" + /> - ))} +
+ + {(showDropdown) && ( +
+ {/* Filtered interests */} + {allInterests + .filter(interest => + interest.name.toLowerCase().includes(newInterest.toLowerCase()) + ) + .map((interest) => ( +
{ + onToggleFilter('interests', interest.name); + 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} + + + ); + })}
- {CAUSE_AREA_OPTIONS.map((cause) => ( + {allCauseAreas.map((cause) => ( ))}
@@ -134,7 +251,10 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese
-
)}
diff --git a/lib/client/media.ts b/lib/client/media.ts new file mode 100644 index 00000000..a932d9ac --- /dev/null +++ b/lib/client/media.ts @@ -0,0 +1,35 @@ +export interface ProfileData { + id: string; + name: string; + image: string; + profile: { + location: string; + gender: string; + personalityType: string; + conflictStyle: string; + description: string; + contactInfo: string; + intellectualInterests: { interest?: { name?: string, id?: string } }[]; + causeAreas: { causeArea?: { name?: string, id?: string } }[]; + desiredConnections: { connection?: { name?: string, id?: string } }[]; + promptAnswers: { prompt?: string; answer?: string, id?: string }[]; + }; +} + +export async function parseImage(img: string, setImage: any) { + if (!img) { + return; + } + if (img.startsWith('http')) { + console.log(`img: ${img}`) + setImage(img); + } else { + const imageResponse = await fetch(`/api/download?key=${img}`); + console.log(`imageResponse: ${imageResponse}`) + if (imageResponse.ok) { + const imageBlob = await imageResponse.json(); + const imageUrl = imageBlob['url']; + setImage(imageUrl); + } + } +} \ No newline at end of file