Authenticated admin endpoint that returns a paginated, searchable list of all waitlist entries and supports full CSV export. Requires role ADMIN.
- Only users with role
ADMINmay access these endpoints. Any authenticated user withoutADMINrole receives403 Forbidden. - The list view shows: queue position, email, name, role, project_idea (truncated), referral code, confirmed referral count, and
created_at. - Search filters by email prefix (case-insensitive
ILIKE). - Pagination is cursor-free — standard page/size with total count returned.
- CSV export downloads the full dataset (all entries, no pagination limit) as a
.csvfile. The browser receives aContent-Disposition: attachment; filename="waitlist-{date}.csv"response header. - Authorization model follows the existing pattern used in invite-system.md: role
ADMINstored inuserstable.
sequenceDiagram
actor Admin
Admin->>+FE: navigates to /admin/waitlist
FE->>+BE: GET /api/v1/admin/waitlist?page=0&size=50&search= <br> Authorization: Bearer {token}
BE->>BE: authenticate and authorize (role = ADMIN)
alt not authenticated
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
alt authenticated but not ADMIN
BE-->>FE: 403 Forbidden <br> ErrorResponse
end
BE->>DB: SELECT entries with position calc, total count <br> ILIKE filter on email if search provided <br> ORDER BY created_at DESC, LIMIT/OFFSET
BE->>-FE: 200 OK <br> WaitlistListResponse
FE->>-Admin: render table with entries, position, referral stats
Admin->>+FE: types email into search box
FE->>FE: debounce 300ms
FE->>+BE: GET /api/v1/admin/waitlist?page=0&size=50&search={email}
BE->>DB: SELECT with ILIKE '%{email}%' filter
BE->>-FE: 200 OK <br> WaitlistListResponse (filtered)
FE->>-Admin: re-render table with filtered results
Admin->>+FE: clicks "Export CSV"
FE->>+BE: GET /api/v1/admin/waitlist/export <br> Authorization: Bearer {token}
BE->>BE: authenticate and authorize (role = ADMIN)
BE->>DB: SELECT all entries (no pagination limit) <br> ORDER BY created_at ASC
BE->>BE: serialize to CSV
BE->>-FE: 200 OK <br> Content-Type: text/csv <br> Content-Disposition: attachment; filename="waitlist-{yyyy-MM-dd}.csv" <br> CSV body
FE->>-Admin: browser triggers file download
GET /api/v1/admin/waitlist — query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 0 | Zero-based page index |
| size | integer | 50 | Entries per page (max 200) |
| search | string | (empty) | Case-insensitive email filter |
200 OK WaitlistListResponse:
{
"data": {
"entries": [
{
"id": 42,
"position": 312,
"email": "jane@example.com",
"name": "Jane Doe",
"role": "MAKER",
"project_idea": "A booking site for my pottery studio",
"referral_code": "janedoe-x4n2",
"referral_url": "https://talkide.app/r/janedoe-x4n2",
"confirmed_referrals": 3,
"referred_by_code": "friend-ab12",
"created_at": "2026-05-16T10:23:00Z"
}
],
"page": 0,
"size": 50,
"total": 2184
}
}
GET /api/v1/admin/waitlist/export — no query parameters (exports full dataset).
200 OK CSV export — response body (plain text, UTF-8 with BOM for Excel compatibility):
position,email,name,role,project_idea,referral_code,confirmed_referrals,referred_by_code,created_at
312,jane@example.com,Jane Doe,MAKER,"A booking site for my pottery studio",janedoe-x4n2,3,friend-ab12,2026-05-16T10:23:00Z
...
401 Unauthorized ErrorResponse:
{
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
403 Forbidden ErrorResponse:
{
"status": 403,
"code": "FORBIDDEN",
"message": "Admin role required"
}
UX Guidelines
User Flow
- Admin je přihlášen a naviguje na
/admin/waitlist(z admin navigace nebo přímé URL). - FE okamžitě spustí
GET /api/v1/admin/waitlist?page=0&size=50— zobrazí se skeleton loader (3 řádky tabulky s pulsujícímvar(--bg-3)bg). - Po načtení dat se skeleton nahradí tabulkou s prvními 50 záznamy. Nad tabulkou se zobrazí počet všech záznamů jako badge.
- Admin může vyhledávat podle emailu — debounce 300ms; při každé změně se stránkování resetuje na page=0 a spustí nový request.
- Admin klikne na “Export CSV” — tlačítko zobrazí spinner, prohlížeč automaticky stáhne soubor
waitlist-{yyyy-MM-dd}.csv. Tlačítko se vrátí do výchozího stavu po dokončení downloadu. - Admin naviguje tabulkou pomocí Previous / Next tlačítek nebo přímým zadáním čísla stránky.
- Admin může kliknout na referral kód v tabulce — zkopíruje se plná referral URL do schránky; buňka krátce zobrazí “Copied!” feedback.
Layout
- Screen type: admin list view
- Responsive: desktop-first; na šíři
< lg(1024px) se skryje sloupec “Project idea”; na< md(768px) layout přejde do card-based výpisu (každý záznam jako karta místo řádku tabulky) - Container: max-width 1400px, padding 32px 24px; stránka používá stejný admin shell jako ostatní admin panely (TopBar/MiniTopBar z
shared.jsx,var(--bg-1)canvas) - Stránka nemá AuthBackdrop — jde o autentizovaný admin kontext
Sekce stránky (shora dolů):
- Page header — titulek “Waitlist” (
var(--font-display)600 24px -0.02em) + count badge (font-mono 11px uppercase,var(--bg-3)bg,var(--line-2)border, border-radius 999, 4px 10px padding,var(--fg-2)) vedle sebe; vpravo “Export CSV” tlačítko (.btn.primarysDownloadikonou — amber bg,var(--primary-fg)text) - Search bar — full-width input,
var(--bg-2)bg,var(--line-2)border, border-radius 10px (var(--r-md)), 12px 16px padding, Search ikona vlevo uvnitř inputu (var(--fg-3)), placeholder “Search by email…”; margin-bottom 16px - Datová tabulka —
var(--bg-2)bg,var(--line-2)border, border-radius 14px (var(--r-lg)), overflow hidden;<thead>svar(--bg-1)bg aborder-bottom: 1px solid var(--line-2); řádky oddělenéborder-bottom: 1px solid var(--line-1); hover řádku: bgvar(--bg-3)transition 0.12s - Pagination bar — flex row, space-between; vlevo “Showing X–Y of Z entries” (13px
var(--fg-3)); vpravo Previous a Next tlačítka (.btn.ghost) + “Page N of M” label (font-mono 12pxvar(--fg-2))
Komponenty
| Element | Komponenta / styl | Poznámka |
|---|---|---|
| Page title | <h1> | var(--font-display) 600 24px -0.02em var(--fg-1) |
| Count badge | <span> pill | font-mono 11px 0.06em uppercase; var(--bg-3) bg, var(--line-2) border, border-radius 999, 4px 10px |
| Export CSV button | .btn.primary | var(--amber) bg, var(--primary-fg) text; Download ikona 14px vlevo; loading: spinner vlevo místo ikony + text “Exporting…”; disabled po dobu exportu |
| Search input | Standalone <input> | Vizuálně konzistentní s AuthInput; var(--bg-2) bg, var(--line-2) border, border-radius 10px; Search ikona 14px var(--fg-3) inline vlevo (padding-left 40px); debounce 300ms |
Tabulka thead | <thead> sticky | var(--bg-1) bg; buňky: font-mono 10px uppercase 0.08em var(--fg-3), 12px 16px padding; kliknutelné buňky (budoucí sort) budou mít cursor: pointer + ChevronUp/Down ikony |
| Sloupec Position | <td> | font-mono 13px, text-align right; top 10: color: var(--amber), font-weight 500; ostatní: var(--fg-2) |
| Sloupec Email | <td> | 13px var(--fg-1) 500; max-width 200px, overflow hidden, text-overflow ellipsis |
| Sloupec Name | <td> | 13px var(--fg-1) |
| Sloupec Role | <td> pill | Pill per role: MAKER — var(--amber-soft) bg var(--amber) text; AGENCY — var(--indigo-soft) bg var(--indigo) text; INTERNAL — var(--green-soft) bg var(--green) text; OTHER — var(--bg-3) bg var(--fg-2) text; font-mono 10px uppercase, border-radius 999 |
| Sloupec Project idea | <td> truncated | Max 80 znaků + ellipsis; title atribut s plným textem pro tooltip; 12px var(--fg-3); skrytý na < lg |
| Sloupec Referral code | <td> kliknutelná | font-mono 12px var(--fg-2) cursor: pointer; klik: kopíruje plnou referral URL; po kopírování buňka krátce zobrazí “Copied!” (zelené) po 2s reset |
| Sloupec Referrals | <td> | 13px; hodnota > 0: var(--amber) 500; hodnota = 0: var(--fg-3) |
| Sloupec Joined | <td> | Relativní čas 12px var(--fg-3) (např. “3 days ago”); title atribut s ISO timestampem |
| Pagination Previous | .btn.ghost | Disabled (opacity 0.4, cursor not-allowed) na první stránce |
| Pagination Next | .btn.ghost | Disabled na poslední stránce |
| Skeleton loader | 3× skeleton řádek | var(--bg-3) bg, border-radius 4px, 36px výška, animace pulse (opacity 0.5 ↔ 1, 1.4s infinite) |
Prázdný stav
Pokud total === 0 (bez search filtru — waitlist je prázdná):
- Tabulka renderuje jediný řádek přes celou šíři s výškou 160px
- Ikona
Users32pxvar(--fg-4)vycentrována vertikálně - Text “No entries yet.” 14px
var(--fg-3)pod ikonou - Žádné akční tlačítko
Pokud total === 0 (se search filtrem — žádný výsledek):
- Stejný layout, text: “No results for “{search}”.”
- Podtext 12px
var(--fg-4): “Try a different email prefix.”
Validační chování
- Search field: žádná validace — libovolný vstup je přijat; trim whitespace před odesláním requestu
- Stránkování: Previous/Next tlačítka jsou disabled (opacity 0.4) na mezích; nelze přejít za rozsah stránek
- Export CSV: tlačítko disabled po celou dobu stahování souboru (spinner); pokud BE vrátí 403, toast “Access denied. Admin role required.” (
var(--rose)bg)
Accessibility
- Tabulka:
<table>srole="grid",<caption>“Waitlist entries” (vizuálně skrytý přes.sr-only),scope="col"na<th>buňkách - Search input:
aria-label="Search waitlist by email",aria-controls="waitlist-table",aria-busy="true"po dobu loadingu - Skeleton rows:
aria-busy="true"na<tbody>po dobu načítání,aria-live="polite"při refresh - Referral code copy:
aria-label="Copy referral link {code}", po kopírováníaria-label="Copied!"po dobu 2s - Export CSV button:
aria-busy="true"po dobu exportu, text “Exporting…” jako viditelný label (ne jen spinner) - Pagination:
aria-label="Previous page"/"Next page";aria-disabled="true"+disabledatribut když není dostupné - Kontrast: amber hodnoty ve sloupcích Position a Referrals splňují WCAG AA na
var(--bg-2)pozadí
Loading a Error stavy
- Prvotní load: skeleton (3 řádky) místo tabulky; počet záznamů badge zobrazí ”—” dokud nedorazí response
- Přechod při search / stránkování: tabulka se zakryje overlay
var(--bg-2) opacity 0.6a spinner po dobu nového requestu (instant replace by byl blikání — overlay je lepší) - Network error / 5xx při load: error banner v místě tabulky — ikona
AlertTriangle20pxvar(--rose), text “Failed to load waitlist. Try again.” + “Retry” button (.btn.ghost) - 403 při mount (přišel non-admin): FE přesměruje na
/dashboardbez renderování stránky; toast “Access denied.” - 401 při mount (token expiroval): FE přesměruje na
/loginsreturnUrl=/admin/waitlist - Export CSV — success: prohlížeč automaticky spustí download; tlačítko se vrátí do výchozího stavu
- Export CSV — error: toast zpráva pravý dolní roh: “CSV export failed. Please try again.”
var(--rose)bg, auto-dismiss 5s
Frontend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| search | optional | 0 - 255 | Debounced 300ms; triggers new page=0 request on change | |
| page | positive_or_zero | Resets to 0 on search change | ||
| size | positive | 1 - 200 | Fixed at 50 in UI; available as URL param |
Backend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| page | not_null, min=0 | Defaults to 0 | ||
| size | not_null, min=1, max=200 | Defaults to 50 | ||
| search | optional | 0 - 255 | Applied as ILIKE ‘%{search}%’ on email column |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| authenticated admin, 50 waitlist entries | list is called (page=0, size=50) | 200 OK; all 50 entries returned with correct positions |
| authenticated admin, search=“jane” | list is called with search param | 200 OK; only entries whose email contains “jane” returned |
| authenticated admin, 2184 entries | export is called | 200 OK; CSV attachment with all 2184 rows, correct headers |
| authenticated admin | export is called | response has Content-Disposition: attachment; filename contains today’s date |
| unauthenticated request | list is called | 401 UNAUTHORIZED |
| authenticated user with role USER (not ADMIN) | list is called | 403 FORBIDDEN |
| authenticated user with role USER (not ADMIN) | export is called | 403 FORBIDDEN |
| page=0, size=0 | list is called | 400 VALIDATION_ERROR (size must be at least 1) |
| page=-1 | list is called | 400 VALIDATION_ERROR (page must be non-negative) |
| size=201 | list is called | 400 VALIDATION_ERROR (size exceeds max 200) |
FEEDBACK
Admin stránka nemá handoff referenci — navrhoval jsem od základu na základě design tokenů a vzorů z ostatních admin panelů (UC-08004). Chybělo mi: (1) reference na skutečný admin shell — existující invite-system.md ho zmiňuje, ale bez vizuálního popisu; bylo by přínosné mít admin-shell.jsx nebo alespoň screenshot. (2) Role color mapping — pro admin table jsem zvolil amber/indigo/green/neutral pro MAKER/AGENCY/INTERNAL/OTHER; pokud má projekt jinou konvenci, je potřeba to sladit.
Thanks for the feedback.