UX Review Punch List -- Jamie (UX/UI Designer)¶
Review date: 2026-04-12 Scope: All implemented screens (login through signage), components, and layouts.
P1 -- Must Fix Before Launch¶
P1-01. No <Button> or <Input> shadcn/ui components -- massive class duplication and consistency risk¶
Every button across the entire app is a raw <button> or <Link> with a hand-copied Tailwind class string (~130 characters each). There is no components/ui/button.tsx and no components/ui/input.tsx. This means:
- A single typo in one copy silently creates a visual inconsistency.
- Adding a design-system-wide change (e.g., border-radius, focus ring color) requires editing dozens of files.
- Taylor is shipping what amounts to a design system bypass.
Files affected: Every page and component file. The primary/secondary/destructive button class strings are duplicated at least 30 times across the codebase.
Action: Generate the standard shadcn/ui Button (with variant and size props) and Input components. Refactor all pages to use them. This is the single highest-leverage fix on this list.
P1-02. Draw list table rows are not keyboard-accessible¶
apps/web/app/(organizer)/[tournamentId]/draws/page.tsx, lines 122-148.
Table rows use onClick with router.push() but have no role="link", no tabIndex, and no onKeyDown handler. A keyboard user cannot navigate to a draw from this table. The same pattern appears in the RR result grid cells (apps/web/app/(organizer)/[tournamentId]/draws/[drawId]/page.tsx, line 732).
Action: Either wrap the row content in an <a> tag (preferred) or add tabIndex={0}, role="link", and onKeyDown for Enter/Space.
P1-03. Seeding page uses HTML5 Drag-and-Drop -- broken on iPad/tablet¶
apps/web/app/(organizer)/[tournamentId]/draws/[drawId]/seed/page.tsx.
The seeding page uses native HTML5 drag events (draggable, onDragStart, onDrop). HTML5 DnD is not supported on mobile Safari or most tablet browsers. The comment at line 18 acknowledges @dnd-kit is reserved for Phase 4, but the seeding page is a critical organizer workflow that must work on a tablet.
Action: Replace HTML5 DnD with @dnd-kit (already a project dependency for Court Assignment). The TouchSensor in @dnd-kit is specifically designed for this.
P1-04. Signage standings table shows truncated UUIDs instead of player names¶
apps/web/app/(public)/d/[id]/page.tsx, line 454.
The LiveStandingsTable component resolves player names as row.playerId.slice(0, 8) + "..." (a truncated UUID). The playerMap is keyed by displayName not playerId, so the lookup never matches. On a TV signage screen, spectators would see a3f7b2c1... instead of player names.
Action: The playerMap needs to be keyed by player ID, or the standings data needs to carry display names. This is a data contract issue between the signage tRPC response and the component.
P1-05. No confirmation before removing a player registration¶
apps/web/app/(organizer)/[tournamentId]/draws/[drawId]/page.tsx, line 294.
The "Remove" button calls removeRegistration.mutate({ id: reg.id }) directly with no confirmation dialog. If a match has already been generated, removing a registration could corrupt bracket integrity. An accidental tap on a tablet is too easy.
Action: Add a confirmation dialog (reuse the existing Dialog component). At minimum, show the player name and warn if matches exist.
P1-06. Court Assignment page unassigned panel is too narrow on iPad portrait¶
apps/web/app/(organizer)/[tournamentId]/schedule/page.tsx, line 439.
The unassigned panel is w-64 (256px) on all viewports, with lg:w-72 (288px) on large screens. On an iPad in portrait mode (768px viewport), this leaves only ~480px for court columns, which is insufficient for more than one column. SCR-005 is specified as a tablet-first design problem.
Action: On tablet viewports, the unassigned panel should either: (a) become a collapsible drawer/sheet that slides in from the left, or (b) sit above the court columns in a horizontal scrolling strip. The current fixed sidebar layout does not work at 768px.
P1-07. Missing aria-label on breadcrumb <nav> elements¶
Multiple files: draws/page.tsx, draws/new/page.tsx, draws/[drawId]/page.tsx, seed/page.tsx.
The breadcrumb <nav> elements have no aria-label="Breadcrumb". Screen readers will announce them as anonymous navigation landmarks, which is confusing when there are multiple <nav> elements on the page (breadcrumb + quick-action nav on Tournament Overview).
Action: Add aria-label="Breadcrumb" to every breadcrumb <nav>.
P1-08. Contact sheet table lacks <caption> and scope attributes¶
apps/web/app/(organizer)/[tournamentId]/contacts/page.tsx, lines 120-160.
The contact sheet table has no <caption> and <th> elements lack scope="col". This is a WCAG 1.3.1 violation. The signage standings tables correctly use scope="col" and <caption className="sr-only">, but the organizer tables do not.
Action: Add <caption className="sr-only"> and scope="col" / scope="row" to all organizer-side data tables (contacts, draws list, registrations, standings).
P2 -- Should Fix¶
P2-01. Inconsistent heading hierarchy across pages¶
- Dashboard:
<h1>is "Tournaments" (text-3xl) - Tournament Overview:
<h1>is tournament name (text-3xl) - Draws List:
<h1>is "Draws" (text-2xl) - Draw Detail:
<h1>is draw name (text-2xl) - Seeding:
<h1>is "Seeding Order" (text-2xl) - Contact Sheet:
<h1>is "Contact Sheet" (text-3xl) - Court Schedule:
<h1>is "Court Schedule" (text-lg)
Court Schedule is visually the odd one out at text-lg. Contact Sheet and Dashboard both use text-3xl which is correct for top-level pages, but Draws List uses text-2xl even though it's also a top-level tournament sub-page.
Action: Standardize: text-3xl for top-level pages (Dashboard, Contact Sheet), text-2xl for tournament sub-pages (Draws, Court Schedule), text-xl for detail pages (Draw Detail, Seeding). Court Schedule header at line 379 needs to change from text-lg to text-2xl.
P2-02. No breadcrumb on Dashboard or Tournament Overview pages¶
- Dashboard has no breadcrumb at all.
- Tournament Overview has no breadcrumb (user has to click the logo to go back).
These are acceptable for v1 since Dashboard is the root, but Tournament Overview should have at least a "< Tournaments" back link for consistency with all other sub-pages.
Action: Add a breadcrumb or back link on Tournament Overview: Dashboard / Tournament Name.
P2-03. "New tournament" page has no breadcrumb¶
apps/web/app/(organizer)/tournaments/new/page.tsx -- no breadcrumb at all. Every other form page (Create Draw, Seeding) has a breadcrumb. The Cancel button navigates to /dashboard, but there's no visual breadcrumb trail.
Action: Add breadcrumb: Dashboard / New Tournament.
P2-04. Loading states are inconsistent¶
- Dashboard: 3
Skeletonbars (h-16) - Tournament Overview: 3
Skeletoncards in a grid (h-32) - Draws List: 3
Skeletonbars (h-10) - Draw Detail: Mixed skeleton sizes
- Court Assignment: Full-height skeletons
The skeleton shapes don't match the eventual content shapes. Dashboard shows h-16 skeletons but the actual cards are taller. This causes layout shift when data arrives.
Action: Match skeleton dimensions to actual content. Use the Skeleton component with realistic heights. For the dashboard, each tournament card is approximately h-24, not h-16.
P2-05. formatDateRange is defined 4 times identically¶
The same helper function formatDateRange(start, end) is copy-pasted in:
- apps/web/app/(organizer)/dashboard/page.tsx (line 111)
- apps/web/app/(organizer)/[tournamentId]/page.tsx (line 167)
- apps/web/app/(organizer)/[tournamentId]/contacts/page.tsx (line 173)
- apps/web/app/(public)/s/[slug]/page.tsx (line 446)
Some use the arrow character, one uses a dash. This is a maintenance risk and a source of visual inconsistency.
Action: Extract to lib/format.ts and import everywhere. Standardize on the arrow character.
P2-06. Error auto-dismiss on Court Assignment uses setTimeout without cleanup¶
apps/web/app/(organizer)/[tournamentId]/schedule/page.tsx, lines 161 and 172.
setTimeout(() => setMoveError(null), 5000) is called inside mutation onError handlers without storing the timeout ID for cleanup. If the component unmounts before 5 seconds, this will attempt to set state on an unmounted component.
Action: Store timeout IDs in a useRef and clear them on unmount or on the next error.
P2-07. Draw type abbreviations ("SE", "RR") are not explained anywhere¶
Badge text "SE" and "RR" appear on Tournament Overview cards and the Draws List table. These abbreviations are clear to tennis organizers but not to all users. There is no tooltip or legend.
Action: Either spell out "Single Elimination" / "Round Robin" (which is done on the Draw Detail page badge), or add a title attribute to the badges.
P2-08. Result entry dialog is not a <form> element¶
apps/web/components/result-entry-dialog.tsx.
The entire result entry UI is built with <div> and standalone <button onClick> handlers rather than a <form onSubmit>. This means:
- Pressing Enter in an input field does not submit the form.
- Browser autofill and form validation features are unavailable.
For a fast data-entry screen used courtside on a tablet, Enter-to-submit is essential.
Action: Wrap the dialog content in a <form onSubmit={handleSubmit}> and change the submit button to type="submit".
P2-09. No visual feedback during realtime subscription connection¶
The useRealtimeInvalidation hook is used on Draw Detail, Court Assignment, and Contact Sheet, but there is no visual indicator of whether the realtime connection is active. If the websocket drops (common on venue Wi-Fi), the organizer has no way to know they are looking at stale data.
Action: Add a small connection status indicator (e.g., a dot in the header) that shows green when connected and amber when disconnected. Not a v1 blocker, but important for trust.
P2-10. Court Assignment match cards have no keyboard-accessible alternative to drag¶
Match cards in Court Assignment have role="button" and tabIndex={0}, but the only way to move a match between courts is via drag-and-drop. There is no keyboard-accessible alternative (e.g., a "Move to..." dropdown or arrow key movement).
Action: Add a "Move to court" context menu or dropdown that appears on right-click or via a keyboard shortcut. This is both an accessibility requirement and a practical fallback when drag-and-drop is finicky.
P2-11. Signage schedule page <meta> tag is placed inside <div> body, not <head>¶
apps/web/app/(public)/s/[slug]/layout.tsx, line 27.
The <meta httpEquiv="refresh" content="30" /> tag is rendered as a child of a <div>, not inside <head>. While most browsers still process it, this is invalid HTML and may not work reliably on all kiosk browsers. The eslint-disable comment acknowledges this.
Action: Use Next.js export const metadata or generateMetadata() to place the refresh in the actual <head>. If that's not possible in a route-group layout, use a client-side setInterval + router.refresh() as a more reliable alternative.
P3 -- Nice to Have¶
P3-01. No favicon or app title configured for signage screens¶
Signage screens showing on a TV typically run in a browser tab. The default Next.js favicon and "Create Next App" title may be visible. A tournament-branded title (e.g., "CourtsideDesk - Juniors U16 2026") would look more professional.
P3-02. Tournament Overview card grid could show match completion progress¶
The draw cards on Tournament Overview show "X matches" but not how many are completed. A progress bar or "3/7 completed" label would give the organizer an at-a-glance status without clicking into each draw.
P3-03. Contact sheet email column could be a mailto: link¶
apps/web/app/(organizer)/[tournamentId]/contacts/page.tsx, line 153.
Email addresses are rendered as plain text. Making them <a href="mailto:..."> links would let organizers tap to email a player directly from the contact sheet on a tablet.
P3-04. Signage result grid column headers show only 3-character abbreviations¶
apps/web/app/(public)/d/[id]/page.tsx, line 903.
The shortName() function takes only the first 3 characters of the first word. For players with similar surnames (e.g., "Mueller" and "Muller"), this produces identical column headers ("Mue" vs "Mul" -- actually "Mue" vs "Mul" works, but "Schmidt" and "Schneider" would both be "Sch").
Action: Consider using last name initial + first few characters, or abbreviating only when the column count demands it.
P3-05. Seeding page "Seed" / "Unseed" buttons are small text links¶
The "Seed" and "Unseed" text-only buttons on the seeding page are functional but have small touch targets (just the text width). On a tablet, these are easy to miss.
Action: Consider making them small icon buttons with a wider touch target, or adding min-w-[44px] min-h-[44px] to meet the WCAG 2.5.5 target size recommendation.
P3-06. Bracket visualization has no connecting lines between rounds¶
apps/web/app/(organizer)/[tournamentId]/draws/[drawId]/page.tsx, line 574.
The SE bracket in the organizer view uses a 4px horizontal line (h-px w-4 bg-border) as a "connector" but there are no vertical lines connecting match pairs to their next-round match. The signage bracket (SCR-002) also lacks lines but uses vertical spacing to imply the tree. For the organizer view on a laptop, connecting lines would significantly improve bracket readability.
P3-07. Print styles for Contact Sheet could be more polished¶
The contact sheet has print:hidden on the header and print button, and print:text-[11px] on the table. Additional print optimizations would be welcome: page break avoidance inside rows, header row repetition on multi-page prints, and removal of hover styles in print.
P3-08. No dark mode toggle for organizer screens¶
The app is light-mode only for organizer screens (PO ruling). However, organizers working late at a venue might appreciate a dark mode option. This is a v2 consideration, not v1.
P3-09. Court management delete uses window.confirm()¶
apps/web/components/court-assignment/court-management.tsx, line 99.
The window.confirm() dialog is browser-native and cannot be styled. It looks out of place next to the rest of the polished UI. Replace with the existing Dialog component for consistency.
P3-10. Signage bracket slots lack score display¶
The signage bracket LiveBracketMatchSlots component (apps/web/app/(public)/d/[id]/page.tsx, line 317) renders player names and winner highlighting, but does not display the actual set scores on the bracket view. Completed matches on signage should show the score line (e.g., "6-4 7-5") next to the bracket slot.
Summary¶
| Priority | Count |
|---|---|
| P1 | 8 |
| P2 | 11 |
| P3 | 10 |
The most impactful fix is P1-01 (extracting Button and Input components). It addresses the root cause of most consistency issues and reduces the diff surface for all future design changes.
The most urgent accessibility fix is P1-02 + P1-07 + P1-08 (keyboard navigation and table semantics).
The most urgent tablet usability fix is P1-03 (seeding DnD) + P1-06 (court assignment panel width).
The most visible bug is P1-04 (UUID display on signage standings).