fix(match): surface replayUrl with new-tab navigation, URL validation, and accessibility#585
Open
petahade wants to merge 4 commits into
Open
fix(match): surface replayUrl with new-tab navigation, URL validation, and accessibility#585petahade wants to merge 4 commits into
petahade wants to merge 4 commits into
Conversation
- Extend `useLeaderboard` with an optional `season` param; appends
`?season=<id>` to every API request and includes it in the React Query
key so season changes trigger fresh fetches rather than reusing cached
data for the wrong season.
- `/leaderboard` page: add a Season filter (alongside category/search/sort),
initialize it from the `?season=` query param on page load, and update
the URL via `router.push(..., { scroll: false })` on each change — no
full reload, back/forward navigation preserved.
- `/leaderboards` page: wire existing `SeasonSelector` to the same URL-sync
handler instead of isolated local state; season is now passed through to
`useLeaderboard` so the API call reflects the selection.
- Both pages use `isFetching` (not just `isLoading`) to show skeleton rows
and a `Loader2` spinner in the table heading while a season-specific
request is in flight, preventing stale rankings from lingering on screen.
- Wrap both page components in a `<Suspense>` boundary required by Next.js
14 for `useSearchParams` in client components.
…RL validation The "Watch Replay" button previously used Next.js <Link>, which routes internally and provides no `target` or `rel`, so clicking it navigated the current tab via the client-side router rather than opening the external replay service. Changes: - Replace <Link> with a native <a> element carrying `target="_blank"` and `rel="noopener noreferrer"` to open replays in a new tab and prevent reverse tabnabbing. - Add `isValidHttpUrl` helper that runs the value through the URL constructor and asserts an http/https protocol, guarding against empty strings, whitespace-only values, and malformed or non-HTTP URLs. - Derive `replayUrl` by trimming the raw field value and `hasReplay` by running validation, so the button is never rendered for invalid inputs. - Add `aria-label="Watch match replay (opens in new tab)"` for keyboard and screen reader users, and mark the decorative Play icon as `aria-hidden="true"` to avoid duplicate announcements.
|
@petahade is attempting to deploy a commit to the paul joseph's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@petahade Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #555
Problem
MatchDetailViewalready had a conditional block formatch.replayUrl, but it was broken in three ways:<Link href={match.replayUrl}>, which pushes the URL through the client-side router. Replay URLs are external (e.g., a video CDN or replay platform), so the tab would navigate away from ArenaX instead of opening a new tab.rel="noopener noreferrer", leaving the replay page able to referencewindow.openerand potentially redirect the parent tab (reverse tabnabbing).match.replayUrl. A whitespace-only string, ajavascript:URI, or a relative path would all pass and produce a broken or unsafe link.Changes
frontend/src/components/match/MatchDetailView.tsxisValidHttpUrlhelper (module-level, before the component)URLconstructor — throws on malformed input, which thecatchconverts tofalse.http:orhttps:protocol, rejectingjavascript:,data:,blob:, and relative strings thatURLmight parse with a base.Derived variables (inside
MatchDetailView)trim()eliminates whitespace-only values before validation.hasReplayis the single gate used in JSX, keeping the render condition readable.Button / link replacement
<a>instead of<Link>— native anchor handles external URLs correctly.target="_blank"opens in a new tab.rel="noopener noreferrer"removeswindow.openerreference and suppresses theRefererheader.aria-labelincludes "(opens in new tab)" so screen reader users know what to expect before activating the link.aria-hidden="true"on the decorativePlayicon prevents the icon name from being announced alongside the label.asChildon<Button>delegates all button styling to the<a>— no wrapper element, no nesting of interactive elements.hasReplayisfalse(null, undefined, empty, whitespace, malformed, non-HTTP), nothing is rendered and no space is reserved in the layout.Acceptance criteria
replayUrlpresent and valid → "Watch Replay" button renderedtarget="_blank")rel="noopener noreferrer")replayUrlisnull/undefined→ no button renderedreplayUrlis""or" "→ no button renderedreplayUrlis malformed (e.g."not a url") → no button renderedURLconstructor throws →false)replayUrluses non-HTTP protocol (e.g."javascript:alert(1)") → no button renderedflex gap-2container, no structural change)<a>is focusable and activatable)aria-labelwith tab-open hint, decorative icon hidden)Test plan
replayUrl— button appears in the header next to "Report Issue".<a>— confirmtarget="_blank"andrel="noopener noreferrer"are present.replayUrl: null/undefined/""/" "/"ftp://example.com"/"javascript:void(0)"/"notaurl"— confirm no button rendered and no layout shift.canDisputeis true, independent of replay state.MatchDetailView(round breakdown, stats cards, prize distribution, bracket link) are unaffected.