feat: Wired /embed/:videoId route outside AppShell for chrome-free rend…

- "frontend/src/App.tsx"
- "frontend/src/pages/WatchPage.tsx"
- "frontend/src/App.css"

GSD-Task: S03/T02
This commit is contained in:
jlightner 2026-04-04 10:59:14 +00:00
parent 8444fbdb12
commit 18e9a4dce1
7 changed files with 163 additions and 5 deletions

View file

@ -25,7 +25,7 @@
- Estimate: 30m - Estimate: 30m
- Files: frontend/src/utils/clipboard.ts, frontend/src/pages/EmbedPlayer.tsx, frontend/src/pages/EmbedPlayer.module.css, frontend/src/pages/ShortPlayer.tsx - 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 - 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`: 1. In `frontend/src/App.tsx`:
- Add lazy import for EmbedPlayer: `const EmbedPlayer = React.lazy(() => import('./pages/EmbedPlayer'))` - Add lazy import for EmbedPlayer: `const EmbedPlayer = React.lazy(() => import('./pages/EmbedPlayer'))`

View file

@ -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
}

View file

@ -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.

View file

@ -6449,6 +6449,13 @@ a.app-footer__about:hover,
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.watch-page__header-top {
display: flex;
align-items: baseline;
gap: 1rem;
flex-wrap: wrap;
}
.watch-page__title { .watch-page__title {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
@ -6456,6 +6463,29 @@ a.app-footer__about:hover,
margin: 0 0 0.25rem; 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 { .watch-page__creator {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--accent); color: var(--accent);

View file

@ -28,6 +28,7 @@ const CreatorTiers = React.lazy(() => import("./pages/CreatorTiers"));
const PostEditor = React.lazy(() => import("./pages/PostEditor")); const PostEditor = React.lazy(() => import("./pages/PostEditor"));
const PostsList = React.lazy(() => import("./pages/PostsList")); const PostsList = React.lazy(() => import("./pages/PostsList"));
const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer")); const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer"));
const EmbedPlayer = React.lazy(() => import("./pages/EmbedPlayer"));
import AdminDropdown from "./components/AdminDropdown"; import AdminDropdown from "./components/AdminDropdown";
import ImpersonationBanner from "./components/ImpersonationBanner"; import ImpersonationBanner from "./components/ImpersonationBanner";
import AppFooter from "./components/AppFooter"; import AppFooter from "./components/AppFooter";
@ -228,7 +229,10 @@ function AppShell() {
export default function App() { export default function App() {
return ( return (
<AuthProvider> <AuthProvider>
<AppShell /> <Routes>
<Route path="/embed/:videoId" element={<Suspense fallback={<LoadingFallback />}><EmbedPlayer /></Suspense>} />
<Route path="/*" element={<AppShell />} />
</Routes>
</AuthProvider> </AuthProvider>
); );
} }

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
import { useParams, useSearchParams, Link } from "react-router-dom"; import { useParams, useSearchParams, Link } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { useMediaSync } from "../hooks/useMediaSync"; import { useMediaSync } from "../hooks/useMediaSync";
@ -10,6 +10,7 @@ import VideoPlayer from "../components/VideoPlayer";
import AudioWaveform from "../components/AudioWaveform"; import AudioWaveform from "../components/AudioWaveform";
import PlayerControls from "../components/PlayerControls"; import PlayerControls from "../components/PlayerControls";
import TranscriptSidebar from "../components/TranscriptSidebar"; import TranscriptSidebar from "../components/TranscriptSidebar";
import { copyToClipboard } from "../utils/clipboard";
export default function WatchPage() { export default function WatchPage() {
const { videoId } = useParams<{ videoId: string }>(); const { videoId } = useParams<{ videoId: string }>();
@ -25,9 +26,20 @@ export default function WatchPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [transcriptError, setTranscriptError] = useState(false); const [transcriptError, setTranscriptError] = useState(false);
const [chapters, setChapters] = useState<Chapter[]>([]); const [chapters, setChapters] = useState<Chapter[]>([]);
const [embedCopied, setEmbedCopied] = useState(false);
const mediaSync = useMediaSync(); const mediaSync = useMediaSync();
const handleCopyEmbed = useCallback(async () => {
const height = video?.video_url ? 405 : 120;
const snippet = `<iframe src="${window.location.origin}/embed/${videoId}" width="720" height="${height}" frameborder="0" allowfullscreen></iframe>`;
const ok = await copyToClipboard(snippet);
if (ok) {
setEmbedCopied(true);
setTimeout(() => setEmbedCopied(false), 2000);
}
}, [videoId, video?.video_url]);
useDocumentTitle(video ? `${video.filename} — Chrysopedia` : "Loading…"); useDocumentTitle(video ? `${video.filename} — Chrysopedia` : "Loading…");
// Fetch video detail // Fetch video detail
@ -106,7 +118,15 @@ export default function WatchPage() {
return ( return (
<div className="watch-page"> <div className="watch-page">
<header className="watch-page__header"> <header className="watch-page__header">
<h1 className="watch-page__title">{video.filename}</h1> <div className="watch-page__header-top">
<h1 className="watch-page__title">{video.filename}</h1>
<button
className={`watch-page__embed-btn${embedCopied ? " watch-page__embed-btn--copied" : ""}`}
onClick={handleCopyEmbed}
>
{embedCopied ? "Copied!" : "Copy Embed Code"}
</button>
</div>
{video.creator_name && video.creator_slug && ( {video.creator_name && video.creator_slug && (
<Link <Link
to={`/creators/${video.creator_slug}`} to={`/creators/${video.creator_slug}`}

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/EmbedPlayer.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx","./src/utils/clipboard.ts"],"version":"5.6.3"}