diff --git a/.gsd/milestones/M024/slices/S03/S03-PLAN.md b/.gsd/milestones/M024/slices/S03/S03-PLAN.md index 55db8f5..4a206b2 100644 --- a/.gsd/milestones/M024/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M024/slices/S03/S03-PLAN.md @@ -25,7 +25,7 @@ - Estimate: 30m - Files: frontend/src/utils/clipboard.ts, frontend/src/pages/EmbedPlayer.tsx, frontend/src/pages/EmbedPlayer.module.css, frontend/src/pages/ShortPlayer.tsx - Verify: cd frontend && npx tsc --noEmit -- [ ] **T02: Wire embed route into App.tsx and add copy-embed button to WatchPage** — Restructure App.tsx to support a chrome-free embed route and add the copy-embed-code button to WatchPage. +- [x] **T02: Wired /embed/:videoId route outside AppShell for chrome-free rendering and added Copy Embed Code button to WatchPage header** — Restructure App.tsx to support a chrome-free embed route and add the copy-embed-code button to WatchPage. 1. In `frontend/src/App.tsx`: - Add lazy import for EmbedPlayer: `const EmbedPlayer = React.lazy(() => import('./pages/EmbedPlayer'))` diff --git a/.gsd/milestones/M024/slices/S03/tasks/T01-VERIFY.json b/.gsd/milestones/M024/slices/S03/tasks/T01-VERIFY.json new file mode 100644 index 0000000..694510b --- /dev/null +++ b/.gsd/milestones/M024/slices/S03/tasks/T01-VERIFY.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M024/S03/T01", + "timestamp": 1775300121772, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 14, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 1060, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M024/slices/S03/tasks/T02-SUMMARY.md b/.gsd/milestones/M024/slices/S03/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..5e0cfa9 --- /dev/null +++ b/.gsd/milestones/M024/slices/S03/tasks/T02-SUMMARY.md @@ -0,0 +1,80 @@ +--- +id: T02 +parent: S03 +milestone: M024 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/App.tsx", "frontend/src/pages/WatchPage.tsx", "frontend/src/App.css"] +key_decisions: ["Embed route rendered at top-level Routes before AppShell fallback for chrome-free iframe rendering", "Audio-only embeds use height 120 vs 405 for video"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "npx tsc --noEmit passed with exit 0. npm run build passed with exit 0, 190 modules transformed, EmbedPlayer code-split into its own chunk." +completed_at: 2026-04-04T10:59:07.670Z +blocker_discovered: false +--- + +# T02: Wired /embed/:videoId route outside AppShell for chrome-free rendering and added Copy Embed Code button to WatchPage header + +> Wired /embed/:videoId route outside AppShell for chrome-free rendering and added Copy Embed Code button to WatchPage header + +## What Happened +--- +id: T02 +parent: S03 +milestone: M024 +key_files: + - frontend/src/App.tsx + - frontend/src/pages/WatchPage.tsx + - frontend/src/App.css +key_decisions: + - Embed route rendered at top-level Routes before AppShell fallback for chrome-free iframe rendering + - Audio-only embeds use height 120 vs 405 for video +duration: "" +verification_result: passed +completed_at: 2026-04-04T10:59:07.670Z +blocker_discovered: false +--- + +# T02: Wired /embed/:videoId route outside AppShell for chrome-free rendering and added Copy Embed Code button to WatchPage header + +**Wired /embed/:videoId route outside AppShell for chrome-free rendering and added Copy Embed Code button to WatchPage header** + +## What Happened + +Restructured App.tsx to render /embed/:videoId at top-level Routes before the AppShell catch-all, so embed pages skip header/nav/footer entirely. Added a Copy Embed Code button to WatchPage header that generates an iframe snippet with audio-aware height (120px for audio-only, 405px for video). Uses shared copyToClipboard utility from T01 with 2-second Copied! feedback. + +## Verification + +npx tsc --noEmit passed with exit 0. npm run build passed with exit 0, 190 modules transformed, EmbedPlayer code-split into its own chunk. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `npx tsc --noEmit` | 0 | ✅ pass | 12800ms | +| 2 | `npm run build` | 0 | ✅ pass | 8100ms | + + +## Deviations + +Added .watch-page__header-top flex container for title/button layout — minor structural addition not in the plan. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/App.tsx` +- `frontend/src/pages/WatchPage.tsx` +- `frontend/src/App.css` + + +## Deviations +Added .watch-page__header-top flex container for title/button layout — minor structural addition not in the plan. + +## Known Issues +None. diff --git a/frontend/src/App.css b/frontend/src/App.css index 4f2ffb7..27ad9f6 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -6449,6 +6449,13 @@ a.app-footer__about:hover, margin-bottom: 1.25rem; } +.watch-page__header-top { + display: flex; + align-items: baseline; + gap: 1rem; + flex-wrap: wrap; +} + .watch-page__title { font-size: 1.25rem; font-weight: 600; @@ -6456,6 +6463,29 @@ a.app-footer__about:hover, margin: 0 0 0.25rem; } +.watch-page__embed-btn { + font-size: 0.8rem; + padding: 0.3rem 0.75rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-alt, var(--surface)); + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.watch-page__embed-btn:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.watch-page__embed-btn--copied { + background: var(--accent); + color: var(--surface); + border-color: var(--accent); +} + .watch-page__creator { font-size: 0.9rem; color: var(--accent); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2dd8a6d..1fa454d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ const CreatorTiers = React.lazy(() => import("./pages/CreatorTiers")); const PostEditor = React.lazy(() => import("./pages/PostEditor")); const PostsList = React.lazy(() => import("./pages/PostsList")); const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer")); +const EmbedPlayer = React.lazy(() => import("./pages/EmbedPlayer")); import AdminDropdown from "./components/AdminDropdown"; import ImpersonationBanner from "./components/ImpersonationBanner"; import AppFooter from "./components/AppFooter"; @@ -228,7 +229,10 @@ function AppShell() { export default function App() { return ( - + + }>} /> + } /> + ); } diff --git a/frontend/src/pages/WatchPage.tsx b/frontend/src/pages/WatchPage.tsx index f61fce9..567ab79 100644 --- a/frontend/src/pages/WatchPage.tsx +++ b/frontend/src/pages/WatchPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { useParams, useSearchParams, Link } from "react-router-dom"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useMediaSync } from "../hooks/useMediaSync"; @@ -10,6 +10,7 @@ import VideoPlayer from "../components/VideoPlayer"; import AudioWaveform from "../components/AudioWaveform"; import PlayerControls from "../components/PlayerControls"; import TranscriptSidebar from "../components/TranscriptSidebar"; +import { copyToClipboard } from "../utils/clipboard"; export default function WatchPage() { const { videoId } = useParams<{ videoId: string }>(); @@ -25,9 +26,20 @@ export default function WatchPage() { const [error, setError] = useState(null); const [transcriptError, setTranscriptError] = useState(false); const [chapters, setChapters] = useState([]); + const [embedCopied, setEmbedCopied] = useState(false); const mediaSync = useMediaSync(); + const handleCopyEmbed = useCallback(async () => { + const height = video?.video_url ? 405 : 120; + const snippet = ``; + const ok = await copyToClipboard(snippet); + if (ok) { + setEmbedCopied(true); + setTimeout(() => setEmbedCopied(false), 2000); + } + }, [videoId, video?.video_url]); + useDocumentTitle(video ? `${video.filename} — Chrysopedia` : "Loading…"); // Fetch video detail @@ -106,7 +118,15 @@ export default function WatchPage() { return ( - {video.filename} + + {video.filename} + + {embedCopied ? "Copied!" : "Copy Embed Code"} + + {video.creator_name && video.creator_slug && (