mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-01-01 18:38:18 -05:00
Fix bad useState practices
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {DropdownKey} from "@/app/profiles/page";
|
||||
import { DropdownKey } from "@/lib/client/schema";
|
||||
|
||||
|
||||
type DropdownProps = {
|
||||
id: DropdownKey
|
||||
|
||||
@@ -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 individual’s 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 platform’s overall benefit (read, increase in total well-being) is then the product of quantity and quality—reaching 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 individual’s 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. That’s 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 future—provided that it doesn’t 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 diminish—making the platform more likely to survive financially.</p>
|
||||
<h3 id="viability">Viability</h3>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
15
lib/client/fetching.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
254
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user