Skip to content

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 Skeleton bars (h-16)
  • Tournament Overview: 3 Skeleton cards in a grid (h-32)
  • Draws List: 3 Skeleton bars (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.


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.


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).