Skip to content

feat(share): leaderboard share affordances + share-my-rank card#6359

Draft
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-leaderboard
Draft

feat(share): leaderboard share affordances + share-my-rank card#6359
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-leaderboard

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 22, 2026

Copy link
Copy Markdown
Member

Part of the sharing visibility initiative. Stacked on PR 1 (#6343) — base is claude/website-sharing-visibility-be6b32, not main.

What changed

1. Share your own rank (the headline)

A rank card above the list on a leaderboard page (/users/[type]), for signed-in viewers who actually have a rank:

You're #12 on the longest streak leaderboard
I'm #12 on the daily.dev longest streak leaderboard. Still climbing.

  • The pre-filled share text is rendered under the headline, so what you'll post is what you see.
  • Top-3 gets the existing TopRankBadge medal and a different line ("Somehow I'm feat: profile modal #2 on the daily.dev longest streak leaderboard. Come take the spot.").
  • Rank uses tabular-nums; the share control is an aria-label'd, keyboard-operable ShareActions trigger (mobile → native share sheet, desktop → share popover).

2. Directory share (/users)

Each leaderboard card gets its own share control next to the card title, sharing that board's permalink. Delivered through a new optional titleAction slot on LeaderboardListContainer.

3. Page-level share (/users/[type])

In the v2 PageHeader actions slot and in the legacy breadcrumb row.

Composition check: the detail page deliberately does not pass titleAction, so the page-level share and the card share never double-render. The directory page has no page-level share for the same reason.

How rank is sourced

The existing leaderboardPosition(type) query (packages/shared/src/graphql/leaderboard.ts) → { rank: number | null, score, cappedAt }. No new field, no invented name.

  • It is viewer-scoped — there is no userId argument; the API resolves the position of the authenticated caller.
  • It is only supported for highestReputation, longestStreak, mostReadingDays (isLeaderboardPositionSupported). Other boards render no rank card.
  • rank is null when the viewer sits past cappedAt. That renders nothing — there is no honest number, so no share affordance. Same for loading, no data, signed-out, and unsupported boards. There is no code path that can produce "I'm #undefined".

useLeaderboardPosition extracts the query CurrentUserPositionRow already ran inline; both now share one cache entry (identical query key).

OG image — deferred, with a backend dependency

Not shipped. Building it now would be half-building it:

  1. No per-user rank on the API. leaderboardPosition takes no userId, so an ISR image-generator page (the screenshot_wrapper pattern in pages/image-generator/) cannot resolve someone else's rank at build time. Deriving it from the top-100 board query would silently break for anyone outside the top 100. Backend dependency: a public leaderboardPosition(type, userId) (or equivalent) field.
  2. Nothing would consume the image. A leaderboard page is statically generated with one shared og:image; it cannot vary per viewer. A per-rank image needs a per-rank shareable URL, which does not exist yet.
  3. The screenshot service must learn the new route — a separate backend change regardless.

Shipping the rank-share affordance now; the image is a clean follow-up once (1) lands.

Flag

share_leaderboard, default false, appended at the end of featureManagement.ts.

Gated via useLeaderboardShareEnabled(): useSharingVisibility() (the initiative kill-switch) is evaluated first, and the per-topic flag is only evaluated with shouldEvaluate: isSharingEnabled — control users are never bucketed into the experiment.

Flag-off is pixel-identical to the PR 1 branch. Both pages compute the flag and pass the slot as undefined when off, so no empty wrapper is introduced: the card heading keeps its original <h3 class="mb-2 …"> directly above the <ol>, and PageHeader renders no actions container. Asserted in tests.

New log events (end of the LogEvent enum): share leaderboard, share leaderboard rank. Reuses the existing Origin.Leaderboard.

Storybook

Components/Share/LeaderboardShare — own rank, top-3, deep rank (4 figures), all three supported boards, the renders-nothing state, and the directory card on/off side by side.

⚠️ Note in the story file: the repo-wide packages/storybook/mock/gb.ts does return value || 'control', so a false default flag reads back as the truthy string 'control' and every flag looks ON in Storybook. These stories therefore never assert a flag — ON states render the presentational components (LeaderboardRankCard, LeaderboardShareControl) directly, and the control state holds FeaturesReadyContext as not-ready. Jest owns the real flag-off guarantee.

Verification

  • node ./scripts/typecheck-strict-changed.js — pass
  • pnpm --filter shared lint — pass
  • pnpm --filter webapp lint — pass
  • NODE_ENV=test pnpm --filter shared test — 297 suites / 2003 tests pass
  • NODE_ENV=test pnpm --filter webapp test — 44 suites / 297 tests pass
  • tsc --noEmit on packages/storybook reports no errors in the new story (package has pre-existing errors elsewhere; pnpm --filter storybook lint cannot run)

Tests cover: copy → clipboard + toast; mobile tap → navigator.share carrying the rank text and tracked link; loading / rank: null / no data / unsupported board / signed-out all render nothing; flag-off renders nothing and leaves the container DOM untouched; and the flag chaining (per-topic flag not evaluated while the kill-switch is off).

🤖 Generated with Claude Code

Preview domain

https://claude-share-leaderboard.preview.app.daily.dev

Adds sharing to the leaderboard surfaces behind `share_leaderboard`, gated
together with the `sharing_visibility` master switch.

- Personal rank share on a leaderboard page: "You're #12 on the longest
  streak leaderboard" with the pre-filled share text spelled out, sourced
  from the existing viewer-scoped `leaderboardPosition(type)` query.
- Per-leaderboard share on every directory card, via a new optional
  `titleAction` slot on `LeaderboardListContainer`.
- Page-level share on a leaderboard page (v2 header + breadcrumb row).

`useLeaderboardPosition` extracts the query that `CurrentUserPositionRow`
already ran, so both surfaces share one cache entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 22, 2026 3:17pm
storybook Building Building Preview Jul 22, 2026 3:17pm

Request Review

Initiative-wide normalization: one event per entity, surfaces distinguished
by payload. Rank shares already carry rank in extra, so the dedicated
ShareLeaderboardRank event was redundant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant