mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-12 02:27:36 -04:00
Upgrade filters
This commit is contained in:
@@ -15,6 +15,7 @@ export async function GET(
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
profile: {
|
||||
include: {
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Set<string>>(new Set());
|
||||
const [newInterest, setNewInterest] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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() {
|
||||
<select
|
||||
id="personalityType"
|
||||
name="personalityType"
|
||||
value={personalityType}
|
||||
value={personalityType || ''}
|
||||
onChange={(e) => setPersonalityType(e.target.value as PersonalityType)}
|
||||
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"
|
||||
>
|
||||
@@ -339,7 +388,7 @@ export default function CompleteProfile() {
|
||||
<select
|
||||
id="conflictStyle"
|
||||
name="conflictStyle"
|
||||
value={conflictStyle}
|
||||
value={conflictStyle || ''}
|
||||
onChange={(e) => 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() {
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Interests
|
||||
</label>
|
||||
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
|
||||
<input
|
||||
@@ -373,8 +422,11 @@ export default function CompleteProfile() {
|
||||
onClick={() => 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"
|
||||
>
|
||||
<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 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
|
||||
@@ -388,12 +440,13 @@ export default function CompleteProfile() {
|
||||
</div>
|
||||
|
||||
{(showDropdown || newInterest) && (
|
||||
<div className="absolute z-10 mt-1 w-full bg-white 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">
|
||||
<div
|
||||
className="absolute z-10 mt-1 w-full bg-white 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 =>
|
||||
{newInterest && !allInterests.some(i =>
|
||||
i.name.toLowerCase() === newInterest.toLowerCase()
|
||||
) && (
|
||||
<div
|
||||
<div
|
||||
className="text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50"
|
||||
onClick={() => 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()}
|
||||
/>
|
||||
<span className="font-normal ml-3 block truncate">
|
||||
@@ -436,7 +490,7 @@ export default function CompleteProfile() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Selected interests */}
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{Array.from(selectedInterests).map(interestId => {
|
||||
@@ -458,7 +512,8 @@ export default function CompleteProfile() {
|
||||
>
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
@@ -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<string | null>(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 <LoadingSpinner />;
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
@@ -42,7 +51,7 @@ export default function ProfilePage() {
|
||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">View and update your profile information</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/complete-profile"
|
||||
href={`/complete-profile?redirect=${encodeURIComponent(pathname)}`}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Edit Profile
|
||||
|
||||
@@ -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<Set<string>>(new Set());
|
||||
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="w-full mb-8">
|
||||
@@ -57,18 +88,22 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese
|
||||
onChange={(e) => onFilterChange('searchQuery', e.target.value)}
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="px-4 py-2 bg-white border rounded-lg hover:bg-gray-50 flex items-center gap-2 whitespace-nowrap"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
||||
</svg>
|
||||
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
||||
</button>
|
||||
@@ -85,47 +120,129 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese
|
||||
onChange={(e) => onFilterChange('gender', e.target.value)}
|
||||
>
|
||||
<option value="">Any Gender</option>
|
||||
{GENDER_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{Object.keys(Gender).map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Interests</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{INTEREST_OPTIONS.map((interest) => (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Interests
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
value={newInterest}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
key={interest}
|
||||
onClick={() => onToggleFilter('interests', interest)}
|
||||
className={`px-3 py-1 text-sm rounded-full ${
|
||||
filters.interests.includes(interest)
|
||||
? 'bg-blue-100 text-blue-800 border border-blue-200'
|
||||
: 'bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200'
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => 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"
|
||||
>
|
||||
{interest}
|
||||
<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>
|
||||
|
||||
{(showDropdown) && (
|
||||
<div
|
||||
className="absolute z-10 mt-1 w-full bg-white 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">
|
||||
{/* Filtered interests */}
|
||||
{allInterests
|
||||
.filter(interest =>
|
||||
interest.name.toLowerCase().includes(newInterest.toLowerCase())
|
||||
)
|
||||
.map((interest) => (
|
||||
<div
|
||||
key={interest.id}
|
||||
className="text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50"
|
||||
onClick={() => {
|
||||
onToggleFilter('interests', interest.name);
|
||||
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);
|
||||
onToggleFilter('interests', interest.name);
|
||||
}}
|
||||
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>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Cause Areas</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CAUSE_AREA_OPTIONS.map((cause) => (
|
||||
{allCauseAreas.map((cause) => (
|
||||
<button
|
||||
key={cause}
|
||||
onClick={() => onToggleFilter('causeAreas', cause)}
|
||||
key={cause.name}
|
||||
onClick={() => onToggleFilter('causeAreas', cause.name)}
|
||||
className={`px-3 py-1 text-sm rounded-full ${
|
||||
filters.causeAreas.includes(cause)
|
||||
filters.causeAreas.includes(cause.name)
|
||||
? 'bg-green-100 text-green-800 border border-green-200'
|
||||
: 'bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{cause}
|
||||
{cause.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -134,7 +251,10 @@ export function ProfileFilters({ filters, onFilterChange, onToggleFilter, onRese
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onReset}
|
||||
onClick={() => {
|
||||
onReset();
|
||||
setSelectedInterests(new Set());
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
Reset Filters
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {notFound, useParams} from "next/navigation";
|
||||
import {useParams} from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import LoadingSpinner from "@/lib/client/LoadingSpinner";
|
||||
import {ProfileData} from "@/lib/client/schema";
|
||||
import {parseImage} from "@/lib/client/media";
|
||||
|
||||
export const dynamic = "force-dynamic"; // This disables SSG and ISR
|
||||
|
||||
@@ -15,47 +16,21 @@ export default function Post() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${id}`);
|
||||
if (!response.ok) {
|
||||
notFound();
|
||||
}
|
||||
const data = await response.json();
|
||||
setUser(data);
|
||||
const img = data.image;
|
||||
console.log(`Data image: ${img}`)
|
||||
|
||||
// If user has an image key, fetch the image
|
||||
if (img) {
|
||||
if (img.startsWith('http')) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
async function fetchImage() {
|
||||
const res = await fetch(`/api/profiles/${id}`);
|
||||
const data = await res.json();
|
||||
setUser(data);
|
||||
console.log('userData', data);
|
||||
if (data?.image) {
|
||||
await parseImage(data.image, setImage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchProfile();
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
fetchImage();
|
||||
}, []);
|
||||
|
||||
if (!user) {
|
||||
notFound();
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
console.log(`Image: ${image}`)
|
||||
@@ -102,7 +77,7 @@ export default function Post() {
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
|
||||
{user?.profile?.desiredConnections && (
|
||||
<div className="mt-3"> <
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Connection</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
@@ -147,7 +122,7 @@ export default function Post() {
|
||||
)}
|
||||
|
||||
{user?.profile?.intellectualInterests && (
|
||||
<div className="mt-3"> <
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Interests</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
@@ -164,7 +139,7 @@ export default function Post() {
|
||||
)}
|
||||
|
||||
{user?.profile?.causeAreas && (
|
||||
<div className="mt-3"> <
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Cause Areas</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
@@ -181,7 +156,7 @@ export default function Post() {
|
||||
)}
|
||||
|
||||
{user?.profile?.promptAnswers && (
|
||||
<div className="mt-3"> <
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Prompt Answers</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
@@ -224,7 +199,6 @@ export default function Post() {
|
||||
{/*</div>*/}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function ProfilePage() {
|
||||
causeAreas: [] as string[],
|
||||
searchQuery: '',
|
||||
});
|
||||
const [debouncedFilters] = useDebounce(filters, 500);
|
||||
|
||||
const fetchProfiles = useCallback(async () => {
|
||||
try {
|
||||
@@ -159,15 +158,6 @@ export default function ProfilePage() {
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Try adjusting your search or filter to find what you're looking for.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetFilters}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Reset all filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
35
lib/client/media.ts
Normal file
35
lib/client/media.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user