From f5c0c71c96b788f09454526d7d8364ed918df851 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Sat, 1 Aug 2026 11:44:48 +0700 Subject: [PATCH] feat(waitlist): search signups by email or referral --- app/api/waitlist/manage/route.ts | 23 +++++++++++++--- app/dashboard/[[...tab]]/page.tsx | 44 +++++++++++++++++++++++++------ lib/waitlist-filter.ts | 14 ++++++++++ tests/waitlist-filter.test.mjs | 14 ++++++++++ 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/app/api/waitlist/manage/route.ts b/app/api/waitlist/manage/route.ts index 1773a7b..a176bb5 100644 --- a/app/api/waitlist/manage/route.ts +++ b/app/api/waitlist/manage/route.ts @@ -2,7 +2,11 @@ import { NextRequest, NextResponse } from "next/server"; import { readSession, authConfigured, SESSION_COOKIE } from "@/lib/session"; import { findOrCreateAccountByEmail, ownsParkedDomain, listDomainSignups } from "@/lib/db"; import { safeDomain } from "@/lib/config"; -import { filterSignupsByStatus, parseWaitlistStatus } from "@/lib/waitlist-filter"; +import { + filterSignupsByQuery, + filterSignupsByStatus, + parseWaitlistStatus, +} from "@/lib/waitlist-filter"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -16,7 +20,7 @@ async function accountId(req: NextRequest): Promise { return null; } -// GET /api/waitlist/manage?dn=[&format=csv][&status=verified|pending] +// GET /api/waitlist/manage?dn=[&format=csv][&status=verified|pending][&q=] // — the signups for one of the caller's parked domains. Each domain's // waitlist is separate (signups.dn). export async function GET(req: NextRequest) { @@ -29,9 +33,13 @@ export async function GET(req: NextRequest) { } const status = parseWaitlistStatus(req.nextUrl.searchParams.get("status")); if (!status) return NextResponse.json({ error: "invalid waitlist status" }, { status: 400 }); + const query = req.nextUrl.searchParams.get("q") || ""; const allSignups = await listDomainSignups(dn); - const signups = filterSignupsByStatus(allSignups, status); + const signups = filterSignupsByQuery( + filterSignupsByStatus(allSignups, status), + query, + ); if (req.nextUrl.searchParams.get("format") === "csv") { const rows = [["email", "verified", "ref", "created_at"]].concat( @@ -45,5 +53,12 @@ export async function GET(req: NextRequest) { }, }); } - return NextResponse.json({ dn, count: signups.length, total: allSignups.length, status, signups }); + return NextResponse.json({ + dn, + count: signups.length, + total: allSignups.length, + status, + query, + signups, + }); } diff --git a/app/dashboard/[[...tab]]/page.tsx b/app/dashboard/[[...tab]]/page.tsx index fa14f59..19a33cd 100644 --- a/app/dashboard/[[...tab]]/page.tsx +++ b/app/dashboard/[[...tab]]/page.tsx @@ -2,7 +2,11 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { useParams } from "next/navigation"; import { copyText } from "@/lib/clipboard"; -import { filterSignupsByStatus, type WaitlistStatus } from "@/lib/waitlist-filter"; +import { + filterSignupsByQuery, + filterSignupsByStatus, + type WaitlistStatus, +} from "@/lib/waitlist-filter"; import MoshpitTlds from "@/components/MoshpitTlds"; // Tab values double as URL slugs: /dashboard/ (the default "page" tab lives at @@ -759,6 +763,7 @@ function WaitlistPanel({ onError }: { onError: (m: string) => void }) { const [active, setActive] = useState(null); const [signups, setSignups] = useState(null); const [status, setStatus] = useState("all"); + const [query, setQuery] = useState(""); const load = async (dn: string) => { setActive(dn); setSignups(null); @@ -783,11 +788,15 @@ function WaitlistPanel({ onError }: { onError: (m: string) => void }) { return

Waitlist

No parked domains yet — claim one on the “Domains” tab and its waitlist shows up here.

; } - const filtered = signups ? filterSignupsByStatus(signups, status) : null; - const verifiedCount = signups?.filter((signup) => signup.verified).length ?? 0; - const pendingCount = signups ? signups.length - verifiedCount : 0; + const matchingSignups = signups ? filterSignupsByQuery(signups, query) : null; + const filtered = matchingSignups + ? filterSignupsByStatus(matchingSignups, status) + : null; + const hasFilters = status !== "all" || query.trim() !== ""; + const verifiedCount = matchingSignups?.filter((signup) => signup.verified).length ?? 0; + const pendingCount = matchingSignups ? matchingSignups.length - verifiedCount : 0; const filters: { value: WaitlistStatus; label: string; count: number }[] = [ - { value: "all", label: "All", count: signups?.length ?? 0 }, + { value: "all", label: "All", count: matchingSignups?.length ?? 0 }, { value: "verified", label: "Confirmed", count: verifiedCount }, { value: "pending", label: "Pending", count: pendingCount }, ]; @@ -807,10 +816,25 @@ function WaitlistPanel({ onError }: { onError: (m: string) => void }) { <> +
+ setQuery(event.target.value)} + placeholder="Search by email or referral code" + aria-label="Search waitlist signups" + /> + {query && ( + + )} +
{filters.map((filter) => (