---
estimated_steps: 53
estimated_files: 6
skills_used: []
---

# T03: Build WatchPage, TranscriptSidebar, route wiring, and TechniquePage timestamp links

Compose the full watch experience: TranscriptSidebar synced to playback, WatchPage layout, route in App.tsx, and clickable timestamp links on TechniquePage.

## Steps

1. Create `frontend/src/api/videos.ts`:
   - `fetchVideo(id: string)` → GET `/api/v1/videos/{id}` returning video detail (id, filename, creator_name, creator_slug, video_url, duration_seconds)
   - `fetchTranscript(videoId: string)` → GET `/api/v1/videos/{videoId}/transcript` returning `{ video_id, segments: [{start_time, end_time, text, segment_index}], total }`
   - Define TypeScript interfaces: `VideoDetail`, `TranscriptSegment`, `TranscriptResponse`
2. Create `frontend/src/components/TranscriptSidebar.tsx`:
   - Props: `segments: TranscriptSegment[]`, `currentTime: number`, `onSeek: (time: number) => void`
   - Renders scrollable list of segments, each showing timestamp (MM:SS) and text
   - Active segment determination: binary search on sorted segments array to find segment where `start_time <= currentTime < end_time` (O(log n) for 500+ segments)
   - Active segment gets `.transcript-segment--active` class with cyan left border highlight
   - Auto-scroll: `useEffect` watching activeSegmentIndex, calls `Element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })` on the active element. Use a ref map or data attribute to find the element.
   - Click handler: clicking a segment calls `onSeek(segment.start_time)`
   - Empty state: if segments array is empty, show "No transcript available"
3. Create `frontend/src/pages/WatchPage.tsx`:
   - Route: `/watch/:videoId` — read videoId from useParams, optional `t` from useSearchParams (parse as float, clamp to 0 if NaN/negative)
   - Fetch video detail and transcript via the API functions
   - Loading/error states
   - Layout: CSS grid — video player (main area) + transcript sidebar (right column on desktop, below on mobile)
   - Instantiate `useMediaSync`, pass to `VideoPlayer` (with `src=video.video_url`, `startTime=t`) and `PlayerControls`
   - Pass transcript segments and mediaSync.currentTime/seekTo to `TranscriptSidebar`
   - Page title: `useDocumentTitle` with video filename
   - Header: show video filename and link to creator detail page
4. Add route to `frontend/src/App.tsx`:
   - Lazy-load WatchPage: `const WatchPage = React.lazy(() => import('./pages/WatchPage'));`
   - Add route: `<Route path="/watch/:videoId" element={<Suspense fallback={<LoadingFallback />}><WatchPage /></Suspense>} />`
   - Place it in the public routes section
5. Update `frontend/src/pages/TechniquePage.tsx`:
   - In the key moments list, wrap each timestamp span in a `<Link>` to `/watch/${km.source_video_id}?t=${km.start_time}`
   - Only link if `km.source_video_id` is truthy
   - Style the timestamp link with cyan color and underline on hover
6. Add styles to `frontend/src/App.css`:
   - `.watch-page` grid layout: `grid-template-columns: 1fr 22rem` on desktop, single column on mobile (768px breakpoint)
   - `.watch-page__header` with video title and creator link
   - `.transcript-sidebar` with max-height, overflow-y auto, scrollbar styling
   - `.transcript-segment` row styling with hover state
   - `.transcript-segment--active` with cyan left border and subtle background
   - `.transcript-segment__time` monospace font
   - Responsive: sidebar below player on mobile
7. Verify: `cd frontend && npx tsc --noEmit && npm run build` — zero errors, WatchPage chunk separate from main.

## Must-Haves

- [ ] TranscriptSidebar uses binary search for active segment (not linear scan)
- [ ] Active segment auto-scrolls into view with smooth behavior
- [ ] Click on segment seeks video to that timestamp
- [ ] WatchPage fetches video + transcript, composes player + sidebar
- [ ] `/watch/:videoId` route registered and lazy-loaded
- [ ] `?t=` query param sets initial seek position
- [ ] TechniquePage timestamps are clickable links to /watch/:videoId?t=X
- [ ] Responsive layout: sidebar beside player on desktop, below on mobile

## Negative Tests

- Malformed inputs: `?t=abc` → clamp to 0, `?t=-5` → clamp to 0
- Error paths: invalid videoId → 404 error state, transcript fetch fails → player still works without sidebar
- Boundary conditions: empty segments array → "No transcript available", video with 500+ segments → binary search keeps performance O(log n)

## Inputs

- ``frontend/src/hooks/useMediaSync.ts` — media sync hook from T02`
- ``frontend/src/components/VideoPlayer.tsx` — player component from T02`
- ``frontend/src/components/PlayerControls.tsx` — controls component from T02`
- ``frontend/src/api/client.ts` — request helper`
- ``frontend/src/pages/TechniquePage.tsx` — existing key moments display`
- ``frontend/src/App.tsx` — existing route definitions`
- ``frontend/src/hooks/useDocumentTitle.ts` — existing page title hook`
- ``frontend/src/App.css` — existing styles with CSS custom properties`
- ``backend/routers/videos.py` — API contract from T01`
- ``backend/schemas.py` — response schemas from T01`

## Expected Output

- ``frontend/src/api/videos.ts` — API client functions for video detail and transcript`
- ``frontend/src/components/TranscriptSidebar.tsx` — time-synced transcript display`
- ``frontend/src/pages/WatchPage.tsx` — watch page composing player + transcript`
- ``frontend/src/App.tsx` — updated with /watch/:videoId route`
- ``frontend/src/pages/TechniquePage.tsx` — updated with clickable timestamp links`
- ``frontend/src/App.css` — updated with watch page and transcript styles`

## Verification

cd frontend && npx tsc --noEmit && npm run build
