Duplication audit — components, pages, in-page information (2026-09-08)
Status: Active · Owner ask: "audit across components and pages to remove duplicate components; streamline and compact the user experience and information; reduce cognitive burden."
Method: three read-only research passes (routes/pages, components across packages/ui → packages/app/presentation → packages/features, in-page information redundancy on the ten main screens), each citing file:line, then a fix batch for everything that removes a duplicate without changing what a screen is for, and a decision list for everything that reshapes a page.
1. Pages and routes
| Finding | Evidence | Outcome |
|---|---|---|
| Standalone pickup list = the 경기 tab's 찾기 pane, verbatim | activity-pickups-screen.tsx reproduced activity-screen.tsx PickupFinderTab (same hooks, controls, cards) | Retired. routes.activityPickups now opens the tab on the pane (/activity?tab=pickup_finder); /activity/pickups (mobile + web) redirects there |
| Standalone upcoming-sessions list = the 내 경기 pane; zero inbound links | activity-upcoming-sessions-screen.tsx; no routes.* helper, no push | Retired; /activity/sessions (web) redirects to the tab |
| Standalone club discover = the 클럽 tab's 탐색 pane, verbatim | clubs-discover-screen.tsx vs club-list-screen.tsx DiscoverTab | Retired. routes.discoverClubs = /clubs?tab=discover; /clubs/discover (mobile + web) redirects |
Club-scoped venue detail = /venues/[id] with an unused clubId | clubs/[clubId]/venues/[venueId].tsx mounted the same VenueDetailScreen, one caller | Deleted; club-venues-screen opens routes.venue |
| 코트 header shortcut on the 경기 tab duplicated the adjacent 코트 pane | CourtsHeaderAction → routes.courts, whose body is the pane already mounted one tap away | Removed (component deleted) |
Broken link: 기록 zero-match CTA pushed /activity/discover-pickups, a path with no route file | records-screen.tsx | Fixed → routes.activityPickups |
| Phantom helpers with no route file and no caller | courtDetail, seasonStandings, clubSeasonStandings, partnerMatching, discoverPickups | Deleted from routes.ts |
/ranking, /leaderboard, /clubs/[clubId]/sessions/[sessionId] (mobile) | one-line redirects for old URL shapes; web 301s the first two | Kept — aliases, not surfaces |
home.tsx, clubs/index.tsx, profile/index.tsx, records/index.tsx, activity/index.tsx | <Redirect> gateways into the grouped tab routes | Kept — the alias layer routes.ts documents |
s/[sessionId], lb/[clubId], m/[matchId], p/[userId] | share/deep-link receivers that resolve then redirect | Kept (note: lb/ lands on the club page, not a leaderboard — rename candidate) |
| match-board / tournament-board / scorecard | host editor, bracket, spectator view — distinct roles, distinct callers | Kept |
Pane vs full screen (club members/sessions, records period/day/history, 코트 pane vs /courts) | documented "see-all" drill-downs; courts-directory-screen.tsx is explicitly one body with two mounts | Kept |
2. Components
The 2026-09-05…08 normalization passes already hold: every sheet is ModalPanel, no raw Modal/bottom-sheet, no TouchableOpacity, one avatar chain, one meter chain, one tier-badge chrome. Six real clusters remained:
| Cluster | Members | Consumers | Outcome |
|---|---|---|---|
| Banner re-implemented for the archived-club notice | club-status-banner.tsx vs ui/banner.tsx | 1 | Banner tone="muted" added; wrapper collapsed |
| StatTile card chrome re-implemented | ui/stat-grid.tsx vs ui/stat-tile.tsx (StatStrip already correct) | 2 | StatTile gains delta; StatGrid composes StatTile |
| Sheet header re-implemented | scorecard/score-sheet-header.tsx vs ui/app-header.tsx | 3 | AppHeader chrome="bare"; file deleted |
Dead Korean-calendar rail duplicating DayCalendarPicker's cell rules | presentation/components/date-rail.tsx (0 renderers) | 0 | Deleted |
| Two hand-rolled single-thumb pan sliders | ui/value-slider.tsx, ui/hue-slider.tsx bar | 7 (unchanged) | One single-band slider core; both render variants of it |
Continue-chip vs SelectionChip (radius differs) | format-chooser-sheet.tsx | 1 | Left — likely intentional radius |
False positives worth recording so nobody re-audits them: VenueRow vs DirectoryVenueCard (different data sources, owner ruling 2026-09-04/07); SessionCompactRow vs SessionCardV2 (embed vs feed card, share RecruitmentMeter); MemberRow vs ListRow (exempted in list-row.tsx); AppHeader / CollapsingTopBar / MainTabShell (three roles); EmptyState → QueryErrorState → OfflineEmptyState (composition, not copies).
3. In-page information redundancy
Fixed in this batch (pure duplicates, no page reshaped):
- Venue detail: share offered twice (icon row + ⋯ menu) → icon row only; reopen-report offered twice (icon row + closed banner) → banner only; the Bookmark glyph meant both "save venue" and "claim ownership" → ownership gets its own glyph.
- Spectator scorecard: the round state word ("진행 중"/"예정") sat in the badge AND the adjacent summary string (two i18n strings for one word) → badge only.
- Club card model computed chip groups, style chips and cadence labels the card never rendered → pruned.
- 경기 tab: header 코트 icon duplicated the 코트 pane (above).
Owner rulings (2026-09-08, boards /wireframes/compaction-*)
| # | Ruling | Follow-through |
|---|---|---|
| D1 | YES — CollapsingTopBar on every detail page with a graphical hero; the hero must not restate a session title; the flat one-fact-per-row metadata under hero titles is rejected in favour of a compact identity pattern applied to every hero; detail-page side spacing must match club detail | Build: shell-level collapse wiring; identity-pattern research → canon → sweep; rail audit |
| D2 | KEEP the StatStrip as the one numbers row, each page choosing its key facts | Canon note; no restatement elsewhere |
| D3 | Key badges live BESIDE the title, never below; header live dot stays | Supersedes hero canon 2026-09-06(g); part of the identity pattern |
| D4 | NO — 정보 stays expanded | Closed |
| D5, D6 | Keep the three full screens AND the 홈 schedule block (else auto-post/pin upcoming meets in the feed) | Closed |
| D7 | YES merge ELO/reliability; heatmap stays OPEN; trend card = delta only | Build |
| D8 | YES merge PillNav + scope selector | Build |
| D9 | YES attendance once + reliability into the ELO sheet; profile meta (클럽, 구력) become badges, with a merged club-logo+name badge | Build after the identity pattern; reconcile the two attendance sources first |
| D10 | NO — title-left, bell-right symmetry stays | Closed |
| D11 | YES rules once per session; open: a visual anchor at the top of live pages | Built (rules once). Anchor proposal drawn: /wireframes/live-anchor — one pinned 진행 중 band (pulse · elapsed · 라운드 N/M · 코트 N) under the header on scorecard, match board, session detail |
| D12 | NO collapse; each detail page gets its own pass | Closed |
4. Prevention
- Route helpers must have a route file and at least one caller;
routes.test.tsnow locks the tab-pane openers so a "full list" screen cannot quietly return beside its pane. - A pane that exists inside a
MainTabShellis the surface; a standalone screen for the same list is a duplicate by definition (canon components.md, Tabs & chips). canon/components.mdrecords the newBanner mutedtone,AppHeader chrome="bare",StatTile delta, and the single-band slider core as the canonical homes.