diff --git a/app/api/profiles/route.ts b/app/api/profiles/route.ts index 31780747..582aead6 100644 --- a/app/api/profiles/route.ts +++ b/app/api/profiles/route.ts @@ -1,22 +1,84 @@ -import { prisma }from "@/lib/server/prisma"; +import { prisma } from "@/lib/server/prisma"; import { NextResponse } from "next/server"; -import {getSession} from "@/lib/server/auth"; +import { getSession } from "@/lib/server/auth"; + +type FilterParams = { + gender?: string; + interests?: string[]; + causeAreas?: string[]; + searchQuery?: string; +}; export async function GET(request: Request) { const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") || "1"); + const gender = url.searchParams.get("gender"); + 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; const session = await getSession(); console.log(`Session: ${session?.user?.name}`); - // Fetch paginated posts + // Build the where clause based on filters + const where: any = { + id: { not: session?.user?.id }, + }; + + if (gender) { + where.profile = { + ...where.profile, + gender: gender, + }; + } + + if (interests.length > 0) { + where.profile = { + ...where.profile, + intellectualInterests: { + some: { + interest: { + name: { in: interests }, + }, + }, + }, + }; + } + + if (causeAreas.length > 0) { + where.profile = { + ...where.profile, + causeAreas: { + some: { + causeArea: { + name: { in: causeAreas }, + }, + }, + }, + }; + } + + if (searchQuery) { + where.OR = [ + { name: { contains: searchQuery, mode: 'insensitive' } }, + { email: { contains: searchQuery, mode: 'insensitive' } }, + { + profile: { + description: { contains: searchQuery, mode: 'insensitive' }, + }, + }, + ]; + } + + // Fetch paginated and filtered profiles const profiles = await prisma.user.findMany({ skip: offset, take: profilesPerPage, orderBy: { createdAt: "desc" }, - where: { id: {not: session?.user?.id} }, + where, select: { id: true, name: true, @@ -31,27 +93,6 @@ export async function GET(request: Request) { }, }, }, - // where: { - // id: { - // not: session?.user?.id, // Exclude the logged-in user - // gender: 'FEMALE', - // intellectualInterests: { - // some: { - // interest: { name: 'Philosophy' } - // } - // }, - // causeAreas: { - // some: { - // causeArea: { name: 'AI Safety' } - // } - // } - // }, - // include: { - // user: true, - // intellectualInterests: { include: { interest: true } }, - // causeAreas: { include: { causeArea: true } } - // } - // }, }); const totalProfiles = await prisma.user.count(); diff --git a/app/profiles/ProfileFilters.tsx b/app/profiles/ProfileFilters.tsx new file mode 100644 index 00000000..84b89fea --- /dev/null +++ b/app/profiles/ProfileFilters.tsx @@ -0,0 +1,147 @@ +'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', +]; + +interface FilterProps { + filters: { + gender: string; + interests: string[]; + causeAreas: string[]; + searchQuery: string; + }; + onFilterChange: (key: string, value: any) => void; + onToggleFilter: (key: 'interests' | 'causeAreas', value: string) => void; + onReset: () => void; +} + +export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onReset }: FilterProps) { + const [showFilters, setShowFilters] = useState(false); + + return ( +
+
+
+ onFilterChange('searchQuery', e.target.value)} + /> +
+ + + +
+
+ + +
+ + {showFilters && ( +
+
+
+ + +
+ +
+ +
+ {INTEREST_OPTIONS.map((interest) => ( + + ))} +
+
+ +
+ +
+ {CAUSE_AREA_OPTIONS.map((cause) => ( + + ))} +
+
+
+ +
+ +
+
+ )} +
+ ); +} diff --git a/app/profiles/page.tsx b/app/profiles/page.tsx index f1c0a20f..b8baca6f 100644 --- a/app/profiles/page.tsx +++ b/app/profiles/page.tsx @@ -1,67 +1,177 @@ 'use client'; import Link from "next/link"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; +import {useDebounce} from "use-debounce"; import LoadingSpinner from "@/lib/client/LoadingSpinner"; import {ProfileData} from "@/lib/client/schema"; - +import {ProfileFilters} from "./ProfileFilters"; // Disable static generation export const dynamic = "force-dynamic"; - export default function ProfilePage() { - const [profiles, setProfiles] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ + gender: '', + interests: [] as string[], + causeAreas: [] as string[], + searchQuery: '', + }); + const [debouncedFilters] = useDebounce(filters, 500); + const fetchProfiles = useCallback(async () => { + try { + setLoading(true); + const params = new URLSearchParams(); + + if (filters.gender) params.append('gender', filters.gender); + if (filters.interests.length > 0) params.append('interests', filters.interests.join(',')); + if (filters.causeAreas.length > 0) params.append('causeAreas', filters.causeAreas.join(',')); + if (filters.searchQuery) params.append('search', filters.searchQuery); + + const response = await fetch(`/api/profiles?${params.toString()}`); + const data = await response.json(); + + if (!response.ok) { + console.error(data.error || 'Failed to fetch profiles'); + return; + } + + setProfiles(data.profiles || []); + } catch (error) { + console.error('Error fetching profiles:', error); + } finally { + setLoading(false); + } + }, [filters]); useEffect(() => { - const fetchProfile = async () => { - try { - const response = await fetch('/api/profiles'); - console.log(response) + fetchProfiles(); + }, [fetchProfiles]); - const data = await response.json(); - console.log(data) + const handleFilterChange = (key: string, value: any) => { + setFilters(prev => ({ + ...prev, + [key]: value + })); + }; - if (!response.ok) { - console.error(data.error || 'Failure'); - } + const toggleFilter = (key: 'interests' | 'causeAreas', value: string) => { + setFilters(prev => ({ + ...prev, + [key]: prev[key].includes(value) + ? prev[key].filter((item: string) => item !== value) + : [...prev[key], value] + })); + }; - const p = data['profiles']; - setProfiles(p); - } catch (error) { - console.error('Upload error:', error); - } - }; - - fetchProfile(); - }, []); - - if (!profiles) return ; + const resetFilters = () => { + setFilters({ + gender: '', + interests: [], + causeAreas: [], + searchQuery: '', + }); + }; return ( -
-

Profiles

-
- {profiles.length > 0 ? - (profiles.map((user) => ( - -
-

{user.name}

- {user?.profile?.description && ( -
-

{user.profile.description}

-
- )} -
- - ))) : ( -
- There are no profiles for this search. Relax the filters or come back later. +
+
+

Profiles

+ + + + {loading ? ( +
+
- ) - } + ) : ( +
+ {profiles.length > 0 ? ( + profiles.map((user) => ( + +
+
+
+

+ {user.name} +

+ {user?.profile?.description && ( +

+ {user.profile.description} +

+ )} + +
+ {user.profile?.intellectualInterests && user.profile.intellectualInterests.length > 0 && ( +
+ {user.profile.intellectualInterests.slice(0, 3).map(({interest}) => ( + + {interest?.name} + + ))} +
+ )} + {user.profile?.causeAreas && user.profile.causeAreas.length > 0 && ( +
+ {user.profile.causeAreas.slice(0, 3).map(({causeArea}) => ( + + {causeArea?.name} + + ))} +
+ )} +
+ +
+
+
+ + )) + ) : ( +
+ +

No profiles found

+

+ Try adjusting your search or filter to find what you're looking for. +

+
+ +
+
+ )} +
+ )}
); diff --git a/lib/client/schema.ts b/lib/client/schema.ts index 50cf947e..92673e80 100644 --- a/lib/client/schema.ts +++ b/lib/client/schema.ts @@ -9,9 +9,9 @@ export interface ProfileData { conflictStyle: string; description: string; contactInfo: string; - intellectualInterests: { interest?: { name?: string } }[]; - causeAreas: { causeArea?: { name?: string } }[]; - desiredConnections: { connection?: { name?: string } }[]; - promptAnswers: { prompt?: string; answer?: 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 }[]; }; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a4b7524c..e8358dd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-dom": "^19.0.0", "react-icons": "^5.5.0", "resend": "^4.7.0", + "use-debounce": "^10.0.5", "uuid": "^11.1.0" }, "devDependencies": { @@ -9308,6 +9309,17 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz", + "integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 568796df..70787ee4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-dom": "^19.0.0", "react-icons": "^5.5.0", "resend": "^4.7.0", + "use-debounce": "^10.0.5", "uuid": "^11.1.0" }, "devDependencies": {