Transcript Viewer Component
Transcript Viewer Component
Section titled “Transcript Viewer Component”The TranscriptViewer component displays episode transcripts in a collapsible, searchable format with speaker formatting, search highlighting, and copy-to-clipboard functionality.
Features
Section titled “Features”- ✅ Collapsible transcript section
- ✅ Client-side search within transcript
- ✅ Highlight search matches
- ✅ Speaker label formatting
- ✅ Copy to clipboard
- ✅ Scrollable content area
- ✅ Keyboard accessible (Escape to clear search)
- ✅ Mobile responsive
Basic Usage
Section titled “Basic Usage”---import TranscriptViewer from '@rejected-media/podcast-framework-core/components/TranscriptViewer.astro';
const episode = await getEpisode(Astro.params.slug);---
<TranscriptViewer transcript={episode.transcript} episodeNumber={episode.episodeNumber}/>Required Props
Section titled “Required Props”episodeNumber
Section titled “episodeNumber”Type: number
Required: Yes
Episode number for component IDs (allows multiple transcripts on one page).
<TranscriptViewer transcript={episode.transcript} episodeNumber={episode.episodeNumber}/>Optional Props
Section titled “Optional Props”transcript
Section titled “transcript”Type: string
Required: No (component returns null if not provided)
The transcript text. Supports speaker formatting with **Speaker A:** pattern.
<TranscriptViewer transcript={episode.transcript} episodeNumber={1}/>segments
Section titled “segments”Type: TranscriptSegment[]
Required: No (not yet implemented)
Timestamped transcript segments for future enhancement.
interface TranscriptSegment { start: number; // Timestamp in seconds end: number; text: string;}Complete Example
Section titled “Complete Example”---import { getEpisode } from '@rejected-media/podcast-framework-core';import TranscriptViewer from '@rejected-media/podcast-framework-core/components/TranscriptViewer.astro';import BaseLayout from '@rejected-media/podcast-framework-core/layouts/BaseLayout.astro';
const episode = await getEpisode(Astro.params.slug);---
<BaseLayout title={episode.title}> <article class="max-w-4xl mx-auto px-4 py-12"> <!-- Episode header --> <h1 class="text-4xl font-bold mb-4">{episode.title}</h1> <p class="text-lg text-gray-600 mb-8">{episode.description}</p>
<!-- Spotify embed --> {episode.spotifyLink && ( <iframe src={getSpotifyEmbedUrl(episode.spotifyLink)} width="100%" height="352" frameborder="0" allowfullscreen class="mb-12" ></iframe> )}
<!-- Transcript --> {episode.transcript && ( <TranscriptViewer transcript={episode.transcript} episodeNumber={episode.episodeNumber} /> )}
<!-- Show notes --> {episode.showNotes && ( <div class="prose max-w-none"> <h2>Show Notes</h2> <BlockContent blocks={episode.showNotes} /> </div> )} </article></BaseLayout>Speaker Formatting
Section titled “Speaker Formatting”The component automatically formats speaker labels:
Input Format
Section titled “Input Format”**Speaker A:** Hello, welcome to the show.**Speaker B:** Thanks for having me!**Speaker A:** Let's dive into the topic.Rendered Output
Section titled “Rendered Output”Speaker A:Hello, welcome to the show.
Speaker B:Thanks for having me!
Speaker A:Let's dive into the topic.Speakers are rendered with:
- Bold, blue text (
text-blue-600) - Block display with spacing
- Accessible semantic markup (
<strong>)
Search Functionality
Section titled “Search Functionality”Search Features
Section titled “Search Features”- Real-time search - Highlights as you type
- Match counting - Shows number of matches
- Case-insensitive - Matches “MEV”, “mev”, “Mev”
- XSS-safe - Uses DOM manipulation (not
innerHTML)
Search UI
Section titled “Search UI”┌──────────────────────────────────────┐│ [🔍 Search transcript...] ││ Found 3 matches │└──────────────────────────────────────┘Highlighted Matches
Section titled “Highlighted Matches”The economics of MEV are complex. ^^^ (highlighted in yellow)Copy to Clipboard
Section titled “Copy to Clipboard”Click “Copy” button to copy full transcript:
┌─────────────────────────────────┐│ Transcript [Copy] [Show] │└─────────────────────────────────┘
Click "Copy"↓┌─────────────────────────────────┐│ Transcript [Copied!✓] [Show]│└─────────────────────────────────┘(Success message for 2 seconds)Component States
Section titled “Component States”Collapsed (Default)
Section titled “Collapsed (Default)”┌─────────────────────────────────┐│ Transcript [Copy] [Show ▼] │└─────────────────────────────────┘Expanded
Section titled “Expanded”┌─────────────────────────────────┐│ Transcript [Copy] [Hide ▲] │├─────────────────────────────────┤│ [🔍 Search transcript...] ││ Found 2 matches │├─────────────────────────────────┤│ ┌───────────────────────────┐ ││ │ Speaker A: │ ││ │ This is the transcript... │ ││ │ │ ││ │ Speaker B: │ ││ │ More transcript text... │ ││ └───────────────────────────┘ ││ (Scrollable, max height 24rem) │└─────────────────────────────────┘Styling
Section titled “Styling”CSS Custom Properties
Section titled “CSS Custom Properties”--color-primary /* Button background, focus ring */Scrollbar Styling
Section titled “Scrollbar Styling”Custom scrollbar for the transcript content:
.max-h-96::-webkit-scrollbar { width: 8px;}
.max-h-96::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px;}
.max-h-96::-webkit-scrollbar-thumb { background: #888; border-radius: 4px;}Custom Styles
Section titled “Custom Styles”<TranscriptViewer transcript={transcript} episodeNumber={1} />
<style is:global> #transcript-section { border-radius: 1rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
#transcript-content mark { background: #fef08a; padding: 0.125rem 0.25rem; border-radius: 0.25rem; }
#transcript-content strong { color: #3b82f6; font-size: 1.125rem; }</style>Accessibility
Section titled “Accessibility”ARIA Attributes
Section titled “ARIA Attributes”<button id="toggle-transcript" aria-expanded="false" aria-controls="transcript-content"> Show Transcript</button>
<div id="transcript-content" class="hidden"> <!-- Transcript --></div>
<input type="text" id="transcript-search" aria-label="Search transcript"/>
<div id="search-results-info" role="status" aria-live="polite"> Found 3 matches</div>Keyboard Navigation
Section titled “Keyboard Navigation”- Tab - Navigate buttons and inputs
- Enter - Toggle transcript
- Escape - Clear search (when focused in search input)
- Arrow keys - Scroll transcript content
Screen Readers
Section titled “Screen Readers”- Button states announced (
aria-expanded) - Search results announced (
aria-live="polite") - Collapsible section labeled (
aria-controls)
Performance
Section titled “Performance”- Bundle Size: ~3 KB (including search logic)
- JavaScript: Required for interactivity
- Search Speed: <10ms for typical transcripts
- Memory: Transcript stored in data attribute
Characteristics:
Transcript length: 10,000 wordsSearch time: ~5msHighlighting: ~10msTotal: <15ms (imperceptible)Customization Examples
Section titled “Customization Examples”Example 1: Auto-expand on Load
Section titled “Example 1: Auto-expand on Load”<TranscriptViewer transcript={transcript} episodeNumber={1} />
<script> document.addEventListener('DOMContentLoaded', () => { const toggleButton = document.getElementById('toggle-transcript'); toggleButton?.click(); // Auto-expand });</script>Example 2: Jump to Transcript from URL
Section titled “Example 2: Jump to Transcript from URL”<TranscriptViewer transcript={transcript} episodeNumber={1} />
<script> // URL: /episode/1#transcript if (window.location.hash === '#transcript') { const toggleButton = document.getElementById('toggle-transcript'); toggleButton?.click();
setTimeout(() => { document.getElementById('transcript-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); }</script>Example 3: Download Transcript
Section titled “Example 3: Download Transcript”<TranscriptViewer transcript={transcript} episodeNumber={1} />
<!-- Add download button --><button id="download-transcript">Download Transcript</button>
<script> const downloadBtn = document.getElementById('download-transcript'); const transcriptContent = document.getElementById('transcript-content');
downloadBtn?.addEventListener('click', () => { const transcript = transcriptContent?.getAttribute('data-transcript'); if (!transcript) return;
const blob = new Blob([transcript], { type: 'text/plain' }); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `episode-${episodeNumber}-transcript.txt`; a.click();
URL.revokeObjectURL(url); });</script>Example 4: Print-friendly Transcript
Section titled “Example 4: Print-friendly Transcript”<TranscriptViewer transcript={transcript} episodeNumber={1} />
<style is:global> @media print { #toggle-transcript, #copy-transcript, #transcript-search-container { display: none !important; }
#transcript-content { display: block !important; max-height: none !important; overflow: visible !important; } }</style>Advanced Usage
Section titled “Advanced Usage”Timestamped Transcript
Section titled “Timestamped Transcript”While segments prop is not yet fully implemented, you can add timestamp links:
---const transcriptWithTimestamps = formatTranscriptWithTimestamps(episode.transcript);---
<TranscriptViewer transcript={transcriptWithTimestamps} episodeNumber={episode.episodeNumber}/>
<script> // Add timestamp click handlers document.querySelectorAll('[data-timestamp]').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const timestamp = parseInt(link.getAttribute('data-timestamp')); seekToTimestamp(timestamp); // Implement for your player }); });</script>Multi-language Support
Section titled “Multi-language Support”Add language toggle for translated transcripts:
---const transcripts = { en: episode.transcript, es: episode.transcriptEs, fr: episode.transcriptFr};
const currentLang = 'en';---
<div class="flex gap-2 mb-4"> <button data-lang="en">English</button> <button data-lang="es">Español</button> <button data-lang="fr">Français</button></div>
<TranscriptViewer transcript={transcripts[currentLang]} episodeNumber={episode.episodeNumber}/>Troubleshooting
Section titled “Troubleshooting”Transcript not showing
Section titled “Transcript not showing”Check that transcript prop has content:
{episode.transcript ? ( <TranscriptViewer transcript={episode.transcript} episodeNumber={episode.episodeNumber} />) : ( <p>Transcript not available for this episode.</p>)}Speaker formatting not working
Section titled “Speaker formatting not working”Ensure speaker labels follow the exact format:
✅ **Speaker A:** Text here✅ **Speaker B:** More text
❌ Speaker A: Text here (no asterisks)❌ **Speaker A** Text here (no colon)❌ ** Speaker A:** Text here (space after asterisks)Copy button not working
Section titled “Copy button not working”Check browser support for Clipboard API:
if (!navigator.clipboard) { console.error('Clipboard API not supported'); // Fallback to textarea method}Search highlighting breaking layout
Section titled “Search highlighting breaking layout”Ensure marks don’t break word boundaries:
#transcript-content mark { display: inline; /* Not block */ white-space: normal; /* Allow wrapping */}Related Components
Section titled “Related Components”- Episode Page Template - Complete episode layout
- BlockContent - For show notes rendering
Next Steps
Section titled “Next Steps”- Episode Pages - Create episode templates
- Sanity CMS - Store transcripts in CMS
- Customization - Override component