Web-player landing: behavior and implementation
Status: implemented on 2026-10-13 on
design/web-player-restructure; see the results below. Reference prototype:pencil/web-player-design/landing/motion-prototype.html. Static design:pencil/web-player-design/landing/landing.pen. Motion rules: Motion.
Goal and boundaries
Logged-out and logged-in visitors who open / see the new Bitrate landing. It has eleven
full-screen scenes and works on desktop, on phones and with touch. It replaces the old landing
(Welcome, QR code, Premium features, Plans, For students), including the plan copy taken from
Spotify.
- Workspace:
apps/web-playeronly.apps/apiis consumed as is:GET /health/ready. - Not included:
- plans and pricing (a separate task with Bitrate's own plans);
- switching the whole app to Unbounded and Inter (separate);
- per-service health and uptime history (needs an API status endpoint first);
- a real AI assistant backend.
Observable behavior and contracts
- Route.
/is served by its own route group with the landing header (scene nav, Dark/Light/Dim theme switch) and the landing footer./auth/*,/login*,/verify-emailand/legal/*keep the current header and footer. Theproxy.tspublic-route rules stay as they are. Static assets the landing needs must not be redirected to login. - Scenes, in the prototype's order and copy:
- Signal
- Play. Save. Return.
- Playlist Workshop
- Your player. Your rules.
- A library with a memory
- Follow the sound
- Same queue. New scene.
- Beyond the play button
- Partners
- Team
- Start
- Navigation.
- Desktop moves between scenes with the wheel, ↑/↓, PageUp/PageDown, Space, Home/End, the side
dots and the header links. The URL hash follows the scene, and a deep link (
/#library) opens that scene. - Phones and touch screens swipe vertically between scenes. The assistant chat, the status panel, the physics fields (partner spheres, team pit) and inputs keep their own native scrolling and dragging.
- Desktop moves between scenes with the wheel, ↑/↓, PageUp/PageDown, Space, Home/End, the side
dots and the header links. The URL hash follows the scene, and a deep link (
- Motion.
- GSAP drives scene enter and leave.
- Three.js renders the particle surface. It loads lazily, only on the client and after first paint.
- Without WebGL the 2D canvas particles and DOM spheres take over.
- With
prefers-reduced-motion: reduceevery scene is shown statically in a scrollable page. There is no 3D, no loops and no autoplay. - The intro logo loader plays once per session.
- Rendering.
- The page renders on the server: headings, copy and links are in the HTML.
ThemeProviderrenders its children before hydration. The inline theme script already sets the theme class, so there is no flash.
- Fonts. Unbounded, Inter and JetBrains Mono load through
next/font, scoped to the landing route. - Language.
- Landing copy exists in en, uk and pl, in a landing-local catalog keyed by the app locale.
- The app ships only en, uk and pl, so every locale has landing copy.
- Calls to action. The whole landing works without an account; the web player does not.
- A guest is never sent to
ROUTES.main(it would bounce to login): the hero and the final scene offer "Create account" (ROUTES.auth.registration) and "Log in" (ROUTES.auth.login). - A signed-in visitor sees "Open web player" (
ROUTES.main) and "Player preview" instead, and no account links.
- A guest is never sent to
- Service status (footer pill and panel). It polls
GET {NEXT_PUBLIC_API_URL}/health/readyevery 60 s while the tab is visible:- 200 → "All systems operational".
- 503 → "Degraded".
- Network error or timeout → "Unreachable".
- Before the first answer → "Checking…".
- The panel shows one row ("Bitrate") with the state and the time of the last check. There is no service list and no uptime bars.
- Demo features stay and are labelled as demo where they show data:
- the assistant with canned answers;
- team bubbles with GitHub links;
- partner placeholder marks.
- Accessibility. Each scene is a labelled region and the counter and dots are buttons with labels. Focus is visible, Esc closes panels, and contrast follows the tokens.
Acceptance and evidence
| ID | Observable criterion | Verification | Result / limitation |
|---|---|---|---|
| AC-1 | / renders the eleven scenes with the prototype's copy; the old landing and Spotify plan copy are gone | e2e: headings of every scene; grep for removed widgets | Pass: e2e asserts all eleven scene regions and no old copy; Welcome, QR code, For students, Plans and Premium features are deleted |
| AC-2 | Server HTML contains the hero heading and CTA links before hydration | e2e: request / with JavaScript disabled | Pass: e2e with JavaScript disabled finds the hero h1 and the Open web player link |
| AC-3 | Wheel, keys, dots and hash navigation move between scenes on desktop | e2e (desktop) | Pass: e2e (desktop) ArrowDown, a dot, the wheel, End and a /#library deep link. Input during a transition is dropped by design; a typed hash waits for it |
| AC-4 | A vertical swipe changes scenes on touch; swipes inside the status panel, assistant and physics fields do not | e2e (mobile, touch events) | Pass: e2e (Pixel 5) a synthetic swipe flips the scene; a swipe on #partnerZone does not |
| AC-5 | Without WebGL the 2D fallbacks render and nothing throws | e2e with WebGL disabled, console clean | Pass: e2e with WebGL context creation blocked: DOM partner bubbles render, no page errors |
| AC-6 | Reduced motion shows a static, scrollable page without 3D or loops | e2e with reducedMotion: 'reduce'; screenshot baseline | Pass: e2e with reduced motion (static, scrollable, no scroll lock, intro hidden); new landing-default.png baseline, reproducible on a second run. The stage canvas is hidden for the shot: masking it painted the whole viewport, so the first baseline checked nothing |
| AC-7 | en, uk and pl copy switch with the app locale | unit test of the landing catalog and fallback | Pass: copy.unit-spec.ts (uk, pl, English fallback for missing keys, interpolation) |
| AC-8 | Status pill reflects /health/ready: 200, 503, network error, initial checking | unit test of the status hook with a mocked fetch | Pass: serviceStatus.unit-spec.ts covers the mapping (pending, 200, 503, network error); the hook is a thin React Query call, not mocked end to end |
| AC-9 | CTAs point at ROUTES; a guest gets account links and no link into the player; a signed-in visitor sees "Open web player" | unit + e2e | Pass: SceneCtas.unit-spec.tsx + e2e with and without a user_access_token cookie |
| AC-10 | Other routes still render after the ThemeProvider change | existing e2e and screenshot suites, type check, lint | Pass: unit 277/277, check-types clean, auth e2e 4/4, next build green after adding Suspense boundaries the server-rendered ThemeProvider exposed (login, verify-email, search, library, header search). The login mobile screenshot differs only by dev-server overlays when run against next start |
| AC-11 | Landing JavaScript for Three.js and GSAP is not in the first-load chunk | next build output: Three.js arrives as a dynamic chunk | Pass: next build + next start: the Three.js and gsap chunks are not among the 17 scripts in the / HTML; they load by dynamic import |
Decisions and approval
The user agreed in this session's interview (2026-10-13):
- Replace the old landing entirely.
- Port the prototype 1:1 with GSAP and Three.js, loaded lazily on the client, with the 2D and static fallbacks.
- Give
/its own route group with the landing header and footer. - Load the landing fonts on the landing only.
- Fix server rendering in
ThemeProvider. - Languages: en, uk and pl only — the app's whole locale set.
- Keep the demo features and label them.
- Work on the current design branch (
design/web-player-restructure). This replaces the recommended separate branch. - Route the calls to action through
ROUTES. - Tests: a static snapshot, e2e for navigation and touch, and unit tests for the logic.
- Service status: the real
/health/readywith a single row, option (a).
Plan confirmation: the user confirmed the plan in the same session on 2026-10-13 ("Подтверждаю, начинай реализацию").
Removed after review
- The header's "Play signal" sound toggle (a WebAudio synth) and everything that reacted to its level: the stage pulse, the spectra lift and the cursor equalizer. The user asked for it to go.
Styling
The landing is styled with Tailwind on the design tokens (the user asked for a full migration after the first port shipped as hand-written CSS):
lib/styles/landing.styles.tsholds the former stylesheet as[marker, utilities]entries in cascade order; context selectors are arbitrary variants ([.win_&]:,max-[820px]:).lc(markers, extra)merges the entries an element carries withcn; former inline styles areextrautilities. Where an inline value used to beat a selector, or a rule was!important, the utility carries Tailwind's!so the cascade is unchanged.- Hex values from the prototype markup map to palette tokens; the brand-mark gradient stays.
- Inside the landing, text inherits the landing fonts (the app's
* { font-family }base rule had put Poppins on unstyled text). - Verification: computed styles of every landing element compared before and after in nine
states (desktop and phone; dark, light and dim; no JavaScript; assistant and status open) on a
production build: identical apart from zero-width border colours and equivalent
background-positionvalues.
Implementation plan
- Dependencies and shell.
- Add
gsap,@gsap/reactandthree(+@types/three) toapps/web-player. - Create the route group
app/(home)/for/, with its layout, landing fonts and metadata. Movepage.tsxout of(landing). - Fix
ThemeProviderso it renders before hydration. - Check that the landing assets pass the proxy matcher.
- Add
- Slice structure (FSD).
views/LandingViewholds the scene composition, the scene engine (navigation, hash, keys, wheel and touch with ignore zones) and the landing catalogmodel/messages.ts(en, uk, pl).- Scenes are
views/LandingView/ui/scenes/*. - Shared, landing-only widgets live in
widgets/Landing*where reuse is plausible: header, footer status, assistant. - The WebGL stage is
views/LandingView/lib/stageand is loaded with dynamicimport(). - The old LandingView pieces and the unused Plans, PremiumFeatures and QR widgets are removed.
- Scenes. Port them in order 1 → 11, each with copy from the catalog and its GSAP enter timeline. Physics (partners, team, cradle) and the logo loader are ported as client modules.
- Status. A
useServiceStatushook (React Query, 60 s polling, paused while hidden) feeds the pill and panel. - Tests.
- Unit: catalog fallback, status mapping, scene index and hash logic.
- e2e: the landing spec rewritten for AC-1 to AC-6 and AC-9.
- Screenshot: the reduced-motion baseline replaces
landing-default.png.
- Checks. These run through
run-heavy.pywhere heavy:pnpm --filter @bitrate/web-player check-types;lintandtest:unit;- e2e and screenshot for landing and smoke routes;
next buildfor chunking.
- Docs. Update
apps/docs(web-player landing notes, specs status). The prototype stays the design reference.
Risks.
- The
ThemeProviderchange affects every route; it is covered by the existing suites, AC-10. - WebGL in CI can be flaky. e2e runs with WebGL disabled plus one SwiftShader smoke check.
- Bundle size: Three.js is lazily loaded, AC-11.
- Rollback is reverting the landing commits; the old view stays in git history.