feat: member portal Sprint 8 — Member Directory
- GET /my/directory in MyService/MyController with q + interest filters - Respects directoryVisible; excludes self; never exposes jid/phone - q searches displayName/hometown/currentLocation (case-insensitive); interest filter via array `has`; aggregates top-20 interest filter chips - BFF route forwards query string - /my/directory page: server-rendered cards (avatar initials, location, interest tags) - DirectorySearch client component: debounced search box + toggleable interest chips that drive URL params - Directory nav item Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
interests: Array<{ name: string; count: number }>;
|
||||
}
|
||||
|
||||
export function DirectorySearch({ interests }: Props) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const activeInterest = searchParams.get('interest');
|
||||
const [q, setQ] = useState(searchParams.get('q') ?? '');
|
||||
|
||||
// Debounced push of the search term into the URL.
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (q.trim()) params.set('q', q.trim());
|
||||
else params.delete('q');
|
||||
router.replace(`/my/directory${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
}, 300);
|
||||
return () => clearTimeout(handle);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [q]);
|
||||
|
||||
const toggleInterest = (name: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (activeInterest === name) params.delete('interest');
|
||||
else params.set('interest', name);
|
||||
router.replace(`/my/directory${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="Search by name, hometown, or location…"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
{interests.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{interests.map((i) => (
|
||||
<button
|
||||
key={i.name}
|
||||
type="button"
|
||||
onClick={() => toggleInterest(i.name)}
|
||||
className={`text-xs rounded-full px-3 py-1 transition-colors ${
|
||||
activeInterest === i.name
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
|
||||
}`}
|
||||
>
|
||||
{i.name} <span className="opacity-60">{i.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user