Fix bad useState practices

This commit is contained in:
MartinBraquet
2025-08-04 14:25:33 +02:00
parent 9bcba9895e
commit a2abc4fda9
9 changed files with 476 additions and 242 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import {ChangeEvent, ReactNode, Suspense, useEffect, useRef, useState} from 'react';
import React, {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';
@@ -10,6 +10,10 @@ import {DeleteProfileButton} from "@/lib/client/profile";
import PromptAnswer from '@/components/ui/PromptAnswer';
import imageCompression from 'browser-image-compression';
import {Item} from '@/lib/client/schema';
import LoadingSpinner from "@/lib/client/LoadingSpinner";
import {fetchFeatures} from "@/lib/client/fetching";
export default function CompleteProfile() {
return (
@@ -51,28 +55,46 @@ function RegisterComponent() {
const router = useRouter();
const {data: session, update} = useSession();
const hooks = Object.fromEntries(['interests', 'coreValues', 'description', 'connections', 'causeAreas'].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);
const featureNames = ['interests', 'coreValues', 'description', 'connections', 'causeAreas'];
return [id, {
showMoreInfo,
setShowMoreInfo,
newFeature,
setNewFeature,
allFeatures,
setAllFeatures,
selectedFeatures,
setSelectedFeatures,
dropdownRef,
showDropdown,
setShowDropdown
}]
}));
const [showMoreInfo, _setShowMoreInfo] = useState(() =>
Object.fromEntries(featureNames.map((id) => [id, false]))
);
const setShowMoreInfo = (id: string, value: boolean) => {
_setShowMoreInfo((prev) => ({...prev, [id]: value}));
};
const [newFeature, _setNewFeature] = useState(() =>
Object.fromEntries(featureNames.map((id) => [id, '']))
);
const setNewFeature = (id: string, value: string) => {
_setNewFeature((prev) => ({...prev, [id]: value}));
};
const [allFeatures, _setAllFeatures] = useState(() =>
Object.fromEntries(featureNames.map((id) => [id, [] as Item[]]))
);
const setAllFeatures = (id: string, value: any) => {
_setAllFeatures((prev) => ({...prev, [id]: value}));
};
const [selectedFeatures, _setSelectedFeatures] = useState(() =>
Object.fromEntries(featureNames.map((id) => [id, new Set<string>()]))
);
const setSelectedFeatures = (id: string, value: Set<string>) => {
_setSelectedFeatures((prev) => ({...prev, [id]: value}));
}
const [showDropdown, _setShowDropdown] = useState(() =>
Object.fromEntries(featureNames.map((id) => [id, false]))
);
const setShowDropdown = (id: string, value: boolean) => {
_setShowDropdown((prev) => ({...prev, [id]: value}));
};
const refDropdown = useRef<any>(
Object.fromEntries(featureNames.map((id) => [id, React.createRef<HTMLDivElement>()]))
);
const id = session?.user.id
@@ -106,18 +128,18 @@ function RegisterComponent() {
}
// Set selected interests if any
function setSelectedFeatures(id: string, attribute: string, subAttribute: string) {
function setSelFeat(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));
setSelectedFeatures(id, new Set(ids));
}
}
setSelectedFeatures('interests', 'intellectualInterests', 'interest')
setSelectedFeatures('coreValues', 'coreValues', 'value')
setSelectedFeatures('connections', 'desiredConnections', 'connection')
setSelectedFeatures('causeAreas', 'causeAreas', 'causeArea')
setSelFeat('interests', 'intellectualInterests', 'interest')
setSelFeat('coreValues', 'coreValues', 'value')
setSelFeat('connections', 'desiredConnections', 'connection')
setSelFeat('causeAreas', 'causeAreas', 'causeArea')
setImages([])
setKeys(profile?.images)
@@ -156,45 +178,32 @@ function RegisterComponent() {
}, []);
// Load existing interests and set up click-outside handler
useEffect(() => {
async function fetchFeatures() {
try {
const res = await fetch('/api/interests');
if (res.ok) {
const data = await res.json();
for (const id of ['interests', 'coreValues', 'connections', 'causeAreas']) {
hooks[id].setAllFeatures(data[id] || []);
// 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);
}
};
document.addEventListener('mousedown', handleClickOutside);
// return () => {
// document.removeEventListener('mousedown', handleClickOutside);
// };
}
}
} catch (error) {
console.error('Error loading' + id, error);
}
}
fetchFeatures();
fetchFeatures(setAllFeatures)
}, []);
useEffect(() => {
// Close dropdown when clicking outside
const handleClickOutside = (event: MouseEvent) => {
for (const id in showDropdown) {
const ref = refDropdown.current[id];
if (
ref?.current &&
!ref.current.contains(event.target as Node)
) {
setShowDropdown(id, false);
}
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showDropdown]);
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>
);
return <LoadingSpinner/>
}
const handleImagesUpload = async (e: ChangeEvent<HTMLInputElement>) => {
@@ -295,9 +304,9 @@ function RegisterComponent() {
...(name && {name}),
};
for (const name of ['interests', 'connections', 'coreValues', 'causeAreas']) {
data[name] = Array.from(hooks[name].selectedFeatures).map(id => ({
data[name] = Array.from(selectedFeatures[name]).map(id => ({
id: id.startsWith('new-') ? undefined : id,
name: hooks[name].allFeatures.find(i => i.id === id)?.name || id.replace('new-', '')
name: allFeatures[name].find(i => i.id === id)?.name || id.replace('new-', '')
}));
}
console.log('data', data)
@@ -327,24 +336,21 @@ function RegisterComponent() {
const genderOptions = Object.values(Gender);
// const personalityOptions = Object.values(PersonalityType);
const conflictOptions = Object.values(ConflictStyle);
// const conflictOptions = Object.values(ConflictStyle);
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)}
onClick={() => setShowMoreInfo(id, !showMoreInfo[id])}
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}
{showMoreInfo[id] ? 'Hide info' : brief}
<svg
className={`w-4 h-4 ml-1 transition-transform ${showMoreInfo ? 'rotate-180' : ''}`}
className={`w-4 h-4 ml-1 transition-transform ${showMoreInfo[id] ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -352,7 +358,7 @@ function RegisterComponent() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
</svg>
</button>
{showMoreInfo && (
{showMoreInfo[id] && (
<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>
@@ -423,27 +429,18 @@ function RegisterComponent() {
]
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 newFeat = newFeature[id];
const allFeat = allFeatures[id];
const selectedFeat = selectedFeatures[id];
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 newSet = new Set(selectedFeat);
if (newSet.has(featureId)) {
newSet.delete(featureId);
} else {
newSet.add(featureId);
}
setSelectedFeatures(id, newSet);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -451,17 +448,17 @@ function RegisterComponent() {
e.preventDefault();
addNewFeature();
} else if (e.key === 'Escape') {
setShowDropdown(false);
setShowDropdown(id, false);
}
};
const addNewFeature = (e?: React.FormEvent) => {
if (e) e.preventDefault();
const toAdd = newFeature.trim();
const toAdd = newFeat.trim();
if (!toAdd) return;
// Check if interest already exists (case-insensitive)
const existingFeature = allFeatures.find(
const existingFeature = allFeat.find(
i => i.name.toLowerCase() === toAdd.toLowerCase()
);
@@ -471,16 +468,16 @@ function RegisterComponent() {
} else {
// Add new feature
const newObj = {id: `new-${Date.now()}`, name: toAdd};
setAllFeatures(prev => [...prev, newObj]);
setSelectedFeatures(prev => new Set(prev).add(newObj.id));
setAllFeatures(id, [...allFeat, newObj]);
setSelectedFeatures(id, new Set(selectedFeat).add(newObj.id));
}
setNewFeature('');
setShowDropdown(false);
setNewFeature(id, '');
setShowDropdown(id, false);
};
return <>
<div className="relative" ref={dropdownRef}>
<div className="relative" ref={refDropdown.current[id]}>
<label className={headingStyle}>
{title}
</label>
@@ -490,17 +487,17 @@ function RegisterComponent() {
<div className="flex items-center border border-gray-300 rounded-md shadow-sm">
<input
type="text"
value={newFeature}
value={newFeat}
maxLength={100}
onChange={(e) => setNewFeature(e.target.value)}
onFocus={() => setShowDropdown(true)}
onChange={(e) => setNewFeature(id, e.target.value)}
onFocus={() => setShowDropdown(id, 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={allowAdd ? "Type to search or add" : "Type to search"}
/>
<button
type="button"
onClick={() => setShowDropdown(!showDropdown)}
onClick={() => setShowDropdown(id, !showDropdown[id])}
className="px-3 py-2 border-l border-gray-300 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 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"
@@ -514,7 +511,7 @@ function RegisterComponent() {
<button
type="button"
onClick={addNewFeature}
disabled={!newFeature.trim()}
disabled={!newFeat.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
@@ -522,12 +519,12 @@ function RegisterComponent() {
}
</div>
{(showDropdown || newFeature) && (
{(showDropdown[id] || newFeat) && (
<div
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 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()
{allowAdd && newFeat && !allFeat.some(i =>
i.name.toLowerCase() === newFeat.toLowerCase()
) && (
<div
className=" cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-blue-50 dark:hover:bg-gray-700"
@@ -535,36 +532,36 @@ function RegisterComponent() {
>
<div className="flex items-center">
<span className="font-normal ml-3 block truncate">
Add "{newFeature}"
Add "{newFeat}"
</span>
</div>
</div>
)}
{/* Filtered interests */}
{allFeatures
.filter(interest =>
interest.name.toLowerCase().includes(newFeature.toLowerCase())
{/* Filtered features */}
{allFeat
.filter(feature =>
feature.name.toLowerCase().includes(newFeat.toLowerCase())
)
.map((interest) => (
.map((feature) => (
<div
key={interest.id}
key={feature.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('');
toggleFeature(feature.id);
setNewFeature(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={selectedFeatures.has(interest.id)}
checked={selectedFeat.has(feature.id)}
onChange={(e) => {
e.stopPropagation()
}}
/>
<span className="font-normal ml-3 block truncate">{interest.name}</span>
<span className="font-normal ml-3 block truncate">{feature.name}</span>
</div>
</div>
))}
@@ -574,8 +571,8 @@ function RegisterComponent() {
{/* Selected interests */}
<div className="flex flex-wrap gap-2 mt-3">
{Array.from(selectedFeatures).map(featureId => {
const interest = allFeatures.find(i => i.id === featureId);
{Array.from(selectedFeat).map(featureId => {
const interest = allFeat.find(i => i.id === featureId);
if (!interest) return null;
return (
<span
@@ -789,25 +786,25 @@ function RegisterComponent() {
{/* </select>*/}
{/*</div>*/}
<div>
<label htmlFor="conflictStyle" className={headingStyle}>
Conflict Style
</label>
<select
id="conflictStyle"
name="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 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
>
<option value="">Select your conflict style</option>
{conflictOptions.map((style) => (
<option key={style} value={style}>
{style}
</option>
))}
</select>
</div>
{/*<div>*/}
{/* <label htmlFor="conflictStyle" className={headingStyle}>*/}
{/* Conflict Style*/}
{/* </label>*/}
{/* <select*/}
{/* id="conflictStyle"*/}
{/* name="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 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"*/}
{/* >*/}
{/* <option value="">Select your conflict style</option>*/}
{/* {conflictOptions.map((style) => (*/}
{/* <option key={style} value={style}>*/}
{/* {style}*/}
{/* </option>*/}
{/* ))}*/}
{/* </select>*/}
{/*</div>*/}
<div className="max-w-3xl w-full">
<label htmlFor="description" className={headingStyle}>

View File

@@ -1,6 +1,7 @@
'use client';
import {DropdownKey} from "@/app/profiles/page";
import { DropdownKey } from "@/lib/client/schema";
type DropdownProps = {
id: DropdownKey

View File

@@ -1,6 +1,7 @@
'use client';
import {aColor, supportEmail} from "@/lib/client/constants";
import Image from 'next/image';
export default function PrivacyPage() {
return (
@@ -169,7 +170,7 @@ export default function PrivacyPage() {
<p>If the platform is very selective, there will be only a couple people with very aligned values. Their well-being will increase a lot, but there are only a few of them. So there is not much overall benefit for society.</p>
<p>If the platform is fully open (everyone can join), there are two possibilities depending on the connection mechanism. If the list of members is opaque and poorly searchable, as in traditional dating apps, it becomes very unlikely to find value-aligned people. Hence, each individuals increase in well-being is negligible, even though there are plenty of people, and there is not much overall benefit for society either.</p>
<p>Qualitatively, the figure below illustrates this trade-off between selectivity and openness for poorly searchable (i.e., Tinder-type) platforms. The quality is the individual increase in well-being, which increases as the platform selects for people strictly following the core values. The quantity is simply the number of users. The platforms overall benefit (read, increase in total well-being) is then the product of quantity and qualityreaching a maximum with a non-extreme selectivity.</p>
<p><img src="https://martinbraquet.com/wp-content/uploads/rational_qualitative.png" alt="" /></p>
<p><Image src="https://martinbraquet.com/wp-content/uploads/rational_qualitative.png" alt="" /></p>
<p>The second possibility appears when the members are fully visible and searchable by anyone. In that case, each member can filter and meaningfully engage with the few people aligning with their values. Each individuals increase in well-being becomes important, and there are plenty of people. So, a fully open and searchable platform may bring a lot of overall benefit for society.</p>
<p>Of course, in practice, there will always be interferences within big communities. And, more importantly, a larger platform requires more resources (i.e., funding) and moderation. Thats why the focus of this article is on a specific community for now. Not only do I identify with the rational / intellectual community, but it is also composed of members who are much more likely to contribute (especially on the tech side), making it a very convenient community. But if it creates much greater good, I think it would be worth considering extending the platform at some point in the far futureprovided that it doesnt negatively dilute the community or create brand identity confusion. More than just creating a higher good, a larger user base means economy of scale: donations may scale proportionally while expense per user would diminishmaking the platform more likely to survive financially.</p>
<h3 id="viability">Viability</h3>

View File

@@ -1,11 +1,13 @@
'use client';
import {useEffect, useRef, useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {Gender} from "@prisma/client";
import Dropdown from "@/app/components/dropdown";
import Slider from '@mui/material/Slider';
import {DropdownKey, RangeKey} from "@/app/profiles/page";
import {DropdownKey, RangeKey} from "@/lib/client/schema";
import {capitalize} from "@/lib/format";
import {Item} from "@/lib/client/schema";
import {fetchFeatures} from "@/lib/client/fetching";
interface FilterProps {
filters: {
@@ -39,59 +41,49 @@ export const rangeConfig: { id: RangeKey, name: string, min: number, max: number
]
export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggleFilter, onReset}: FilterProps) {
interface Item {
id: DropdownKey;
name: string;
}
const [showFilters, setShowFilters] = useState(true);
const dropDownStates = Object.fromEntries(dropdownConfig.map(({id}) => {
const [all, setAll] = useState<Item[]>([]);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [newValue, setNewValue] = useState('');
const ref = useRef<HTMLDivElement>(null);
const [show, setShow] = useState(false);
// Initialize state for all dropdowns as an object with keys from dropdownConfig ids
const [optionsDropdown, setOptionsDropdown] = useState(() =>
Object.fromEntries(dropdownConfig.map(({id}) => [id, [] as Item[]]))
);
const setOptionsDropdownId = (id: string, value: any) => {
setOptionsDropdown((prev) => ({...prev, [id]: value}));
};
const [selectedDropdown, setSelectedDropdown] = useState(() =>
Object.fromEntries(dropdownConfig.map(({id}) => [id, new Set<string>()]))
);
const [newDropdown, setNewDropdown] = useState(() =>
Object.fromEntries(dropdownConfig.map(({id}) => [id, '']))
);
const [showDropdown, setShowDropdown] = useState(() =>
Object.fromEntries(dropdownConfig.map(({id}) => [id, false]))
);
// refs cannot be in state; create refs map outside state
const refDropdown = useRef<any>(
Object.fromEntries(dropdownConfig.map(({id}) => [id, React.createRef<HTMLDivElement>()]))
);
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 fetchOptions() {
try {
const res = await fetch('/api/interests');
if (res.ok) {
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);
}
}
} catch (error) {
console.error('Error loading options:', error);
}
}
fetchOptions();
fetchFeatures(setOptionsDropdownId)
}, []);
useEffect(() => {
// Close dropdown when clicking outside
const handleClickOutside = (event: MouseEvent) => {
for (const id in dropDownStates) {
const dropdown = dropDownStates[id];
const ref = dropdown.ref;
for (const id in showDropdown) {
const ref = refDropdown.current[id];
if (
ref?.current &&
!ref.current.contains(event.target as Node)
) {
dropdown.show?.set?.(false); // Defensive chaining
setShowDropdown(prev => ({...prev, [id]: false}));
}
}
};
@@ -101,61 +93,60 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
}, [showDropdown]);
const toggle = (id: DropdownKey, optionId: string) => {
dropDownStates[id].selected.set(prev => {
const newSet = new Set(prev);
setSelectedDropdown(prev => {
const newSet = new Set(prev[id]);
if (newSet.has(optionId)) {
newSet.delete(optionId);
} else {
newSet.add(optionId);
}
return newSet;
return {...prev, [id]: newSet};
});
};
const handleKeyDown = (id: DropdownKey, key: string) => {
if (key === 'Escape') dropDownStates[id].show.set(false);
if (key === 'Escape') setShowDropdown(prev => ({...prev, [id]: false}));
};
const handleChange = (id: DropdownKey, e: string) => {
dropDownStates[id].new.set(e);
setNewDropdown(prev => ({...prev, [id]: e}));
}
const handleFocus = (id: DropdownKey) => {
dropDownStates[id].show.set(true);
setShowDropdown(prev => ({...prev, [id]: true}))
}
const handleClick = (id: DropdownKey) => {
const shown = dropDownStates[id].show.value;
dropDownStates[id].show.set(!shown);
setShowDropdown(prev => ({...prev, [id]: !showDropdown[id]}))
}
function getDrowDown(id: DropdownKey, name: string) {
return (
<div key={id + '.div'}>
<div className="relative" ref={dropDownStates[id].ref}>
<div className="relative" ref={refDropdown.current[id]}>
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-2">
{name}
</label>
<Dropdown
key={id}
id={id}
value={dropDownStates[id].new.value}
value={newDropdown[id]}
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
onClick={handleClick}
/>
{(dropDownStates[id].show.value) && (
{(showDropdown[id]) && (
<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">
{dropDownStates[id].options.value
.filter(v => v.name.toLowerCase().includes(dropDownStates[id].new.value.toLowerCase()))
{optionsDropdown[id]
.filter(v => v.name.toLowerCase().includes(newDropdown[id].toLowerCase()))
.map((v) => (
<div
key={v.id}
@@ -169,7 +160,7 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
checked={dropDownStates[id].selected.value.has(v.id)}
checked={selectedDropdown[id].has(v.id)}
onChange={() => {
}}
onClick={(e) => e.stopPropagation()}
@@ -184,8 +175,8 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
)}
</div>
<div className="flex flex-wrap gap-2 mt-3">
{Array.from(dropDownStates[id].selected.value).map(vId => {
const value = dropDownStates[id].options.value.find(i => i.id === vId);
{Array.from(selectedDropdown[id]).map(vId => {
const value = optionsDropdown[id].find(i => i.id === vId);
if (!value) return null;
return (
<span
@@ -223,21 +214,20 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
max: number;
}
const rangeStates = Object.fromEntries(rangeConfig.map(({id}) => {
const [minVal, setMinVal] = useState<number | undefined>(undefined);
const [maxVal, setMaxVal] = useState<number | undefined>(undefined);
return [id, {
minVal,
maxVal,
setMinVal,
setMaxVal,
}];
}))
const [minRange, setMinRange] = useState(() =>
Object.fromEntries(rangeConfig.map(({id}) => [id, undefined]))
);
const [maxRange, setMaxRange] = useState(() =>
Object.fromEntries(rangeConfig.map(({id}) => [id, undefined]))
);
function getSlider({id, name, min, max}: Range) {
const minStr = 'min' + capitalize(id);
const maxStr = 'max' + capitalize(id);
const {minVal, maxVal, setMinVal, setMaxVal} = rangeStates[id];
const minVal = minRange[id];
const maxVal = maxRange[id];
const setMinVal = (v: any) => setMinRange({...minRange, [id]: v});
const setMaxVal = (v: any) => setMaxRange({...maxRange, [id]: v});
return (
<div key={id + '.div'}>
@@ -376,13 +366,15 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
<button
onClick={() => {
onReset();
Object.values(dropDownStates).map((v) => {
v.selected.set(new Set());
});
Object.values(rangeStates).map((v) => {
v.setMaxVal(undefined);
v.setMinVal(undefined);
});
setSelectedDropdown(() =>
Object.fromEntries(dropdownConfig.map(({id}) => [id, new Set<string>()]))
);
setMinRange(() =>
Object.fromEntries(rangeConfig.map(({id}) => [id, undefined]))
);
setMaxRange(() =>
Object.fromEntries(rangeConfig.map(({id}) => [id, undefined]))
);
}}
className="px-4 py-2 text-sm text-gray-600 dark:text-white hover:text-gray-800"
>

View File

@@ -3,7 +3,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 {DropdownKey, ProfileData} from "@/lib/client/schema";
import {dropdownConfig, ProfileFilters} from "./ProfileFilters";
import Image from "next/image";
@@ -26,9 +26,6 @@ const initialState = {
forceRun: false,
};
export type DropdownKey = 'interests' | 'causeAreas' | 'connections' | 'coreValues';
export type RangeKey = 'age' | 'introversion';
// type OtherKey = 'gender' | 'searchQuery';
export default function ProfilePage() {
const [profiles, setProfiles] = useState<ProfileData[]>([]);

15
lib/client/fetching.ts Normal file
View File

@@ -0,0 +1,15 @@
export async function fetchFeatures(setAllFeatures: any) {
// results = []
try {
const res = await fetch('/api/interests');
if (res.ok) {
const data = await res.json();
for (const [id, values] of Object.entries(data)) {
setAllFeatures(id, values || []);
// results.push({id, values});
}
}
} catch (error) {
console.error('Error fetching feature options:', error);
}
}

View File

@@ -21,4 +21,14 @@ export interface ProfileData {
promptAnswers: { prompt?: string; answer?: string, id?: string }[];
images: string[];
};
}
export type DropdownKey = 'interests' | 'causeAreas' | 'connections' | 'coreValues';
export type RangeKey = 'age' | 'introversion';
// type OtherKey = 'gender' | 'searchQuery';
export interface Item {
id: DropdownKey;
name: string;
}

254
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@heroicons/react": "^2.2.0",
"@mui/material": "^7.2.0",
"@next-auth/prisma-adapter": "^1.0.7",
"@playwright/test": "^1.54.2",
"@prisma/client": "^6.12.0",
"@prisma/extension-accelerate": "^2.0.2",
"@radix-ui/react-select": "^2.2.5",
@@ -43,7 +44,8 @@
"resend": "^4.7.0",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"wait-on": "^8.0.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -54,7 +56,6 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/testing-library__jest-dom": "^6.0.0",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"jest": "^30.0.5",
@@ -2581,6 +2582,21 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
@@ -4131,6 +4147,20 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"dependencies": {
"playwright": "1.54.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -4751,6 +4781,27 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@sinclair/typebox": {
"version": "0.34.38",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz",
@@ -5970,16 +6021,6 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true
},
"node_modules/@types/testing-library__jest-dom": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-6.0.0.tgz",
"integrity": "sha512-bnreXCgus6IIadyHNlN/oI5FfX4dWgvGhOPvpr7zzCYDGAPIfvyIoAozMBINmhmsVuqV0cncejF2y5KC7ScqOg==",
"deprecated": "This is a stub types definition. @testing-library/jest-dom provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"@testing-library/jest-dom": "*"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -6960,6 +7001,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -6986,6 +7033,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -7661,6 +7719,18 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -8029,6 +8099,15 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -8427,7 +8506,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -9318,6 +9396,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -9351,6 +9449,43 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -9746,7 +9881,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -11557,6 +11691,19 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
@@ -11867,8 +12014,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
@@ -12101,7 +12247,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -13000,6 +13145,47 @@
"pathe": "^2.0.3"
}
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"dependencies": {
"playwright-core": "1.54.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -13227,6 +13413,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -13748,6 +13940,15 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -15429,6 +15630,25 @@
"node": ">=18"
}
},
"node_modules/wait-on": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.4.tgz",
"integrity": "sha512-8f9LugAGo4PSc0aLbpKVCVtzayd36sSCp4WLpVngkYq6PK87H79zt77/tlCU6eKCLqR46iFvcl0PU5f+DmtkwA==",
"license": "MIT",
"dependencies": {
"axios": "^1.11.0",
"joi": "^17.13.3",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",

View File

@@ -22,6 +22,7 @@
"@heroicons/react": "^2.2.0",
"@mui/material": "^7.2.0",
"@next-auth/prisma-adapter": "^1.0.7",
"@playwright/test": "^1.54.2",
"@prisma/client": "^6.12.0",
"@prisma/extension-accelerate": "^2.0.2",
"@radix-ui/react-select": "^2.2.5",
@@ -48,7 +49,8 @@
"resend": "^4.7.0",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"wait-on": "^8.0.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -59,7 +61,6 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/testing-library__jest-dom": "^6.0.0",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"jest": "^30.0.5",