diff --git a/apps/web/app/(app)/people/page.tsx b/apps/web/app/(app)/people/page.tsx new file mode 100644 index 0000000..21900b2 --- /dev/null +++ b/apps/web/app/(app)/people/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import { PeopleSearch } from "@/components/social/people-search"; + +export const metadata: Metadata = { title: "Find People — Epicure" }; + +export default function PeoplePage() { + return ( +
+
+

Find People

+

Search for cooks to follow by name or username.

+
+ +
+ ); +} diff --git a/apps/web/app/api/v1/users/search/route.ts b/apps/web/app/api/v1/users/search/route.ts new file mode 100644 index 0000000..977aac7 --- /dev/null +++ b/apps/web/app/api/v1/users/search/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db, users, userBlocks, eq, and, or, ne, ilike, isNotNull } from "@epicure/db"; +import { requireSession } from "@/lib/api-auth"; + +export async function GET(req: NextRequest) { + const { session, response } = await requireSession(); + if (response) return response; + + const q = req.nextUrl.searchParams.get("q")?.trim().slice(0, 50) ?? ""; + if (q.length < 2) return NextResponse.json({ users: [] }); + + const blocked = await db + .select({ blockedId: userBlocks.blockedId }) + .from(userBlocks) + .where(eq(userBlocks.blockerId, session!.user.id)); + const blockedByMe = new Set(blocked.map((b) => b.blockedId)); + + const blockedMe = await db + .select({ blockerId: userBlocks.blockerId }) + .from(userBlocks) + .where(eq(userBlocks.blockedId, session!.user.id)); + const haveBlockedMe = new Set(blockedMe.map((b) => b.blockerId)); + + const rows = await db + .select({ + id: users.id, + name: users.name, + username: users.username, + avatarUrl: users.avatarUrl, + bio: users.bio, + }) + .from(users) + .where( + and( + isNotNull(users.username), + ne(users.id, session!.user.id), + or(ilike(users.name, `%${q}%`), ilike(users.username, `%${q}%`)) + ) + ) + .limit(20); + + const filtered = rows.filter((u) => !blockedByMe.has(u.id) && !haveBlockedMe.has(u.id)); + + return NextResponse.json({ users: filtered }); +} diff --git a/apps/web/components/search/explore-page-content.tsx b/apps/web/components/search/explore-page-content.tsx index 1752498..b113ecc 100644 --- a/apps/web/components/search/explore-page-content.tsx +++ b/apps/web/components/search/explore-page-content.tsx @@ -244,7 +244,12 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
{/* Search bar */}
-

{t("title")}

+
+

{t("title")}

+ + Find people + +
([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (q.trim().length < 2) { + setResults([]); + return; + } + const timeout = setTimeout(async () => { + setLoading(true); + try { + const res = await fetch(`/api/v1/users/search?q=${encodeURIComponent(q.trim())}`); + if (res.ok) { + const data = (await res.json()) as { users: PersonResult[] }; + setResults(data.users); + } + } finally { + setLoading(false); + } + }, 300); + return () => clearTimeout(timeout); + }, [q]); + + return ( +
+
+ + setQ(e.target.value)} + placeholder="Search people by name or username…" + className="pl-9" + /> +
+ + {loading &&

Searching…

} + + {!loading && q.trim().length >= 2 && results.length === 0 && ( +

No one found.

+ )} + +
+ {results.map((person) => ( +
+ + + {person.avatarUrl && } + {person.name.slice(0, 2).toUpperCase()} + +
+

{person.name}

+

@{person.username}

+
+ + {person.username && ( + + )} +
+ ))} +
+
+ ); +}