mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-24 08:49:39 -04:00
Add connections and cause areas to filter
This commit is contained in:
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
const maxAge = url.searchParams.get("maxAge");
|
||||
const interests = url.searchParams.get("interests")?.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") || "";
|
||||
|
||||
const profilesPerPage = 100;
|
||||
@@ -37,11 +38,11 @@ export async function GET(request: Request) {
|
||||
...where.profile,
|
||||
birthYear: {}
|
||||
};
|
||||
|
||||
|
||||
if (minAge) {
|
||||
where.profile.birthYear.lte = currentYear - parseInt(minAge);
|
||||
}
|
||||
|
||||
|
||||
if (maxAge) {
|
||||
where.profile.birthYear.gte = currentYear - parseInt(maxAge);
|
||||
}
|
||||
@@ -65,11 +66,11 @@ export async function GET(request: Request) {
|
||||
if (interests.length > 0) {
|
||||
where.profile = {
|
||||
...where.profile,
|
||||
AND: interests.map((interestName) => ({
|
||||
AND: interests.map((name) => ({
|
||||
intellectualInterests: {
|
||||
some: {
|
||||
interest: {
|
||||
name: interestName,
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -77,19 +78,46 @@ export async function GET(request: Request) {
|
||||
};
|
||||
}
|
||||
|
||||
if (causeAreas.length > 0) {
|
||||
// OR
|
||||
if (connections.length > 0) {
|
||||
where.profile = {
|
||||
...where.profile,
|
||||
causeAreas: {
|
||||
desiredConnections: {
|
||||
some: {
|
||||
causeArea: {
|
||||
name: {in: causeAreas},
|
||||
connection: {
|
||||
name: {in: connections},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (causeAreas.length > 0) {
|
||||
// where.profile = {
|
||||
// ...where.profile,
|
||||
// causeAreas: {
|
||||
// some: {
|
||||
// causeArea: {
|
||||
// name: {in: causeAreas},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
where.profile = {
|
||||
...where.profile,
|
||||
AND: causeAreas.map((name) => ({
|
||||
causeAreas: {
|
||||
some: {
|
||||
causeArea: {
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (searchQuery) {
|
||||
where.OR = [
|
||||
{name: {contains: searchQuery, mode: 'insensitive'}},
|
||||
@@ -118,7 +146,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
// Fetch paginated and filtered profiles
|
||||
const cacheStrategy = { swr: 60, ttl: 60 , tags: ["profiles"]};
|
||||
const cacheStrategy = {swr: 60, ttl: 60, tags: ["profiles"]};
|
||||
const profiles = await prisma.user.findMany({
|
||||
skip: offset,
|
||||
take: profilesPerPage,
|
||||
@@ -127,7 +155,7 @@ export async function GET(request: Request) {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
// email: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
profile: {
|
||||
|
||||
@@ -7,9 +7,7 @@ type DropdownProps = {
|
||||
onChange: (id: string, value: string) => void
|
||||
onFocus?: (id: string) => void
|
||||
onKeyDown?: (id: string, key: string) => void
|
||||
onClick?: (id: string) => void
|
||||
setShowDropdown: (id: boolean) => void
|
||||
showDropdown: boolean
|
||||
onClick: (id: string) => void
|
||||
}
|
||||
|
||||
export default function Dropdown(
|
||||
@@ -20,8 +18,7 @@ export default function Dropdown(
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
setShowDropdown,
|
||||
showDropdown,
|
||||
onClick,
|
||||
}: DropdownProps
|
||||
) {
|
||||
return (
|
||||
@@ -38,7 +35,7 @@ export default function Dropdown(
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
onClick={(e) => onClick?.(id)}
|
||||
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"
|
||||
|
||||
@@ -8,6 +8,7 @@ interface FilterProps {
|
||||
filters: {
|
||||
gender: string;
|
||||
interests: string[];
|
||||
connections: string[];
|
||||
causeAreas: string[];
|
||||
searchQuery: string;
|
||||
minAge?: number | null;
|
||||
@@ -15,145 +16,147 @@ interface FilterProps {
|
||||
};
|
||||
onFilterChange: (key: string, value: any) => void;
|
||||
onShowFilters: (value: boolean) => void;
|
||||
onToggleFilter: (key: 'interests' | 'causeAreas', value: string) => void;
|
||||
onToggleFilter: (key: string, value: string) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export const dropdownConfig = [
|
||||
{id: "interests", name: "Core Interests"},
|
||||
{id: "connections", name: "Desired Connections"},
|
||||
{id: "causeAreas", name: "Cause Areas"},
|
||||
]
|
||||
|
||||
export function ProfileFilters({filters, onFilterChange, onShowFilters, 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 [allConnections, setAllConnections] = 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('');
|
||||
|
||||
const dropDownStates = Object.fromEntries(dropdownConfig.map(({id}) => {
|
||||
const [all, setAll] = useState<{ id: string, name: string }[]>([]);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [newValue, setNewValue] = useState('');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
return [id, {
|
||||
options: {value: all, set: setAll},
|
||||
selected: {value: selected, set: setSelected},
|
||||
new: {value: newValue, set: setNewValue},
|
||||
ref: ref,
|
||||
show: {value: show, set: setShow},
|
||||
}]
|
||||
}))
|
||||
console.log(dropDownStates)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInterests() {
|
||||
async function fetchOptions() {
|
||||
try {
|
||||
const res = await fetch('/api/interests');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAllInterests(data.interests || []);
|
||||
setAllCauseAreas(data.causeAreas || []);
|
||||
setAllConnections(data.desiredConnections || []);
|
||||
console.log('All interests:', data.interests);
|
||||
console.log('All cause areas:', data.causeAreas);
|
||||
console.log('All Connections:', data.desiredConnections);
|
||||
// console.log('Gender', Gender);
|
||||
console.log(data);
|
||||
for (const [id, values] of Object.entries(data)) {
|
||||
dropDownStates[id].options.set(values);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading interests:', error);
|
||||
console.error('Error loading options:', error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchInterests();
|
||||
fetchOptions();
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setShowDropdown(false);
|
||||
for (const id in dropDownStates) {
|
||||
const dropdown = dropDownStates[id];
|
||||
const ref = dropdown.ref;
|
||||
if (
|
||||
ref?.current &&
|
||||
!ref.current.contains(event.target as Node)
|
||||
) {
|
||||
dropdown.show?.set?.(false); // Defensive chaining
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const toggleInterest = (interestId: string) => {
|
||||
setSelectedInterests(prev => {
|
||||
const toggle = (id: string, optionId: string) => {
|
||||
dropDownStates[id].selected.set(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(interestId)) {
|
||||
newSet.delete(interestId);
|
||||
if (newSet.has(optionId)) {
|
||||
newSet.delete(optionId);
|
||||
} else {
|
||||
newSet.add(interestId);
|
||||
newSet.add(optionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (key: string) => {
|
||||
if (key === 'Escape') {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
const handleKeyDown = (id: string, key: string) => {
|
||||
if (key === 'Escape') dropDownStates[id].show.set(false);
|
||||
};
|
||||
|
||||
const dropdownConfig = [
|
||||
{id: "interests",},
|
||||
]
|
||||
|
||||
const [values, setValues] = useState<Record<string, string>>({
|
||||
v: "",
|
||||
// showDropdown: false,
|
||||
})
|
||||
|
||||
const handleChange = (id: string, value: string) => {
|
||||
setValues((prev) => ({ ...prev, [id]: value }))
|
||||
const handleChange = (id: string, e: string) => {
|
||||
dropDownStates[id].new.set(e);
|
||||
}
|
||||
|
||||
const handleFocus = (id: string) => {
|
||||
console.log(`Focused: ${id}`)
|
||||
setShowDropdown[id](true)
|
||||
dropDownStates[id].show.set(true);
|
||||
}
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
const shown = dropDownStates[id].show.value;
|
||||
dropDownStates[id].show.set(!shown);
|
||||
}
|
||||
|
||||
function getDrowDown(id: string, name: string) {
|
||||
|
||||
function getDrowDown() {
|
||||
return (
|
||||
<div>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div key={id + '.div'}>
|
||||
<div className="relative" ref={dropDownStates[id].ref}>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-2">
|
||||
Core Interests
|
||||
{name}
|
||||
</label>
|
||||
<Dropdown
|
||||
key={id}
|
||||
id={id}
|
||||
value={dropDownStates[id].new.value}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
|
||||
{dropdownConfig.map(({ id }) => (
|
||||
<Dropdown
|
||||
key={id}
|
||||
id={id}
|
||||
// options={options}
|
||||
value={values[id]}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
setShowDropdown={() => setShowDropdown[id]}
|
||||
showDropdown={}
|
||||
/>
|
||||
))}
|
||||
|
||||
{(showDropdown) && (
|
||||
{(dropDownStates[id].show.value) && (
|
||||
<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">
|
||||
{/* Filtered interests */}
|
||||
{allInterests
|
||||
.filter(interest =>
|
||||
interest.name.toLowerCase().includes(newInterest.toLowerCase())
|
||||
)
|
||||
.map((interest) => (
|
||||
{dropDownStates[id].options.value
|
||||
.filter(v => v.name.toLowerCase().includes(dropDownStates[id].new.value.toLowerCase()))
|
||||
.map((v) => (
|
||||
<div
|
||||
key={interest.id}
|
||||
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={() => {
|
||||
onToggleFilter('interests', interest.name);
|
||||
toggleInterest(interest.id);
|
||||
// setNewInterest('');
|
||||
onToggleFilter(id, v.name);
|
||||
toggle(id, v.id);
|
||||
}}
|
||||
>
|
||||
<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();
|
||||
}}
|
||||
checked={dropDownStates[id].selected.value.has(v.id)}
|
||||
onChange={() => {}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="font-normal ml-3 block truncate">
|
||||
{interest.name}
|
||||
{v.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,25 +165,25 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
|
||||
)}
|
||||
</div>
|
||||
<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;
|
||||
{Array.from(dropDownStates[id].selected.value).map(vId => {
|
||||
const value = dropDownStates[id].options.value.find(i => i.id === vId);
|
||||
if (!value) return null;
|
||||
return (
|
||||
<span
|
||||
key={interestId}
|
||||
key={vId}
|
||||
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
{interest.name}
|
||||
{value.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleInterest(interestId);
|
||||
onToggleFilter('interests', interest.name);
|
||||
toggle(id, vId);
|
||||
onToggleFilter(id, value.name);
|
||||
}}
|
||||
className="ml-1.5 inline-flex items-center justify-center h-4 w-4 rounded-full bg-blue-200 hover:bg-blue-300 dark:text-white dark:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<span className="sr-only">Remove {interest.name}</span>
|
||||
<span className="sr-only">Remove {value.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"/>
|
||||
@@ -297,7 +300,7 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getDrowDown()}
|
||||
{dropdownConfig.map(({ id, name }) => getDrowDown(id, name))}
|
||||
|
||||
{/*<div>*/}
|
||||
{/* <label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">Cause Areas</label>*/}
|
||||
@@ -324,7 +327,9 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
|
||||
<button
|
||||
onClick={() => {
|
||||
onReset();
|
||||
setSelectedInterests(new Set());
|
||||
Object.values(dropDownStates).map((v) => {
|
||||
v.selected.set(new Set());
|
||||
});
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-600 dark:text-white hover:text-gray-800"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import LoadingSpinner from "@/lib/client/LoadingSpinner";
|
||||
import {ProfileData} from "@/lib/client/schema";
|
||||
import {ProfileFilters} from "./ProfileFilters";
|
||||
import {ProfileFilters, dropdownConfig} from "./ProfileFilters";
|
||||
|
||||
// Disable static generation
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -17,6 +17,7 @@ const initialState = {
|
||||
maxAge: null as number | null,
|
||||
interests: [] as string[],
|
||||
causeAreas: [] as string[],
|
||||
connections: [] as string[],
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
@@ -49,8 +50,12 @@ export default function ProfilePage() {
|
||||
if (filters.gender) params.append('gender', filters.gender);
|
||||
if (filters.minAge) params.append('minAge', filters.minAge.toString());
|
||||
if (filters.maxAge) params.append('maxAge', filters.maxAge.toString());
|
||||
if (filters.interests.length > 0) params.append('interests', filters.interests.join(','));
|
||||
if (filters.causeAreas.length > 0) params.append('causeAreas', filters.causeAreas.join(','));
|
||||
|
||||
for (let i = 0; i < dropdownConfig.length; i++) {
|
||||
const v = dropdownConfig[i];
|
||||
if (filters[v.id].length > 0) params.append(v.id, filters[v.id].join(','))
|
||||
}
|
||||
|
||||
if (filters.searchQuery) params.append('search', filters.searchQuery);
|
||||
|
||||
const response = await fetch(`/api/profiles?${params.toString()}`);
|
||||
@@ -108,7 +113,7 @@ export default function ProfilePage() {
|
||||
setShowFilters(value);
|
||||
};
|
||||
|
||||
const toggleFilter = (key: 'interests' | 'causeAreas', value: string) => {
|
||||
const toggleFilter = (key: string, value: string) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[key]: prev[key].includes(value)
|
||||
|
||||
Reference in New Issue
Block a user