1<!-- new --> 2<!DOCTYPE html> 3<html lang="en" data-theme="light"> 4 5<head> 6 <meta charset="UTF-8" /> 7 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /> 9 <link rel="preconnect" href="https://cdn.tailwindcss.com" crossorigin /> 10 <link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin /> 11 <link rel="preconnect" href="https://js.stripe.com" crossorigin /> 12 <link rel="dns-prefetch" href="https://m.stripe.network" /> 13 <link rel="dns-prefetch" href="https://b.stripecdn.com" /><title>Get any Youtube Transcript | Transcript.you</title> 14
14<script async data-id="101459384" src="//static.getclicky.com/js"></script>
14 15 16 <!-- Google tag (gtag.js) -->
vendor: 68 bytes, lines 16-17
16 17 <script async src="https://www.googletagmanager.com/gtag/js?id=
17AW-634605019
vendor: 16 bytes, lines 17-18
17"></script> 18
18<script> 19
vendor: 155 bytes, lines 19-23
19window.dataLayer = window.dataLayer || []; 20 function gtag() { dataLayer.push(arguments); } 21 gtag('js', new Date()); 22 23 gtag('config', '
23AW-634605019
vendor: 8 bytes, lines 23-24
23'); 24
24</script>
24 25 26
26<script> 27 // ================================================================ 28 // Universal upgrade-modal click tracker + iOS-Safari-safe body lock. 29 // Each viewer template defines its own window.showUpgradeModal; 30 // we trap the property setter so EVERY assignment is wrapped and 31 // EVERY call fires a tracking event with a location label. 32 // ================================================================ 33 (function () { 34 window.__trackUpgradeOpen = function (location, extraMeta) { 35 var loc = location || window.location.pathname || 'unknown'; 36 var meta = Object.assign({location: loc}, extraMeta || {}); 37 try { 38 fetch('/api/track', { 39 method: 'POST', 40 headers: {'Content-Type': 'application/json'}, 41 body: JSON.stringify({event: 'upgrade_modal_open', meta: meta}) 42 }); 43 } catch (e) {} 44 if (window.gtag) { 45 gtag('event', 'upgrade_modal_open', meta); 46 } 47 }; 48 49 var _impl = null; 50 Object.defineProperty(window, 'showUpgradeModal', { 51 configurable: true, 52 get: function () { return _impl; }, 53 set: function (fn) { 54 _impl = function () { 55 var args = Array.prototype.slice.call(arguments); 56 var loc = (typeof args[0] === 'string') ? args[0] : null; 57 try { window.__trackUpgradeOpen(loc); } catch (e) {} 58 return fn.apply(this, args); 59 }; 60 } 61 }); 62 63 document.addEventListener('click', function (e) { 64 var t = e.target && e.target.closest && e.target.closest('[data-upgrade-cta]'); 65 if (!t) return; 66 try { window.__trackUpgradeOpen(t.getAttribute('data-upgrade-cta')); } catch (_) {} 67 }, true); 68 69 // ============================================================ 70 // iOS-Safari-safe body lock for any open <dialog> on the page. 71 // 72 // The CSS :has() / overscroll-behavior trick fails on iOS Safari 73 // with embedded iframes â touches inside Stripe's iframe still 74 // scroll the page underneath. The bulletproof workaround: 75 // when a dialog opens, save scrollY and pin body with 76 // position:fixed; top:-scrollY; width:100%. On close, restore. 77 // Watches every <dialog> via a single MutationObserver so 78 // nothing has to opt in. 79 // ============================================================ 80 window.__lockBodyScroll = function () { 81 if (document.body.dataset.lockedScrollY) return; // already locked 82 var y = window.scrollY || window.pageYOffset || 0; 83 document.body.dataset.lockedScrollY = String(y); 84 document.body.style.position = 'fixed'; 85 document.body.style.top = '-' + y + 'px'; 86 document.body.style.left = '0'; 87 document.body.style.right = '0'; 88 document.body.style.width = '100%'; 89 }; 90 window.__unlockBodyScroll = function () { 91 var y = document.body.dataset.lockedScrollY; 92 if (!y) return; 93 document.body.style.position = ''; 94 document.body.style.top = ''; 95 document.body.style.left = ''; 96 document.body.style.right = ''; 97 document.body.style.width = ''; 98 delete document.body.dataset.lockedScrollY; 99 window.scrollTo(0, parseInt(y, 10) || 0); 100 }; 101 102 function anyDialogOpen() { 103 return !!document.querySelector('dialog[open]'); 104 } 105 function reconcileBodyLock() { 106 if (anyDialogOpen()) window.__lockBodyScroll(); 107 else window.__unlockBodyScroll(); 108 } 109 110 // Watch every dialog's `open` attribute so we react to .showModal(), 111 // .show(), .close(), and ::backdrop dismiss equally. 112 function watchDialogs() { 113 document.querySelectorAll('dialog').forEach(function (d) { 114 if (d.dataset._lockWatched) return; 115 d.dataset._lockWatched = '1'; 116 new MutationObserver(reconcileBodyLock) 117 .observe(d, {attributes: true, attributeFilter: ['open']}); 118 d.addEventListener('close', reconcileBodyLock); 119 }); 120 } 121 if (document.readyState === 'loading') { 122 document.addEventListener('DOMContentLoaded', watchDialogs); 123 } else { 124 watchDialogs(); 125 } 126 // New dialogs added later (some templates inject them). 127 new MutationObserver(function () { watchDialogs(); reconcileBodyLock(); }) 128 .observe(document.documentElement, {childList: true, subtree: true}); 129 })(); 130 </script>
130 131 132
132<script async type="text/javascript" 133 src="https://s3.us-east-2.amazonaws.com/telemetry.bydata/transcript.you/bydataAnalytics.js?cId=asc202860"></script>
133 134 135 136<link rel="canonical" 137 href="https://transcript.you/" /> 138 139 140 141 142 143 144 145 146 147 148 149<meta name="robots" content="index, follow"> 150 151<meta name="title" 152 content="Get a YouTube Transcript - Transcript.you" /> 153 154 155<meta name="description" 156 content="Easily get the full transcript of any YouTube video for free. Our tool allows you to convert YouTube videos to text, with clickable timestamps and multi-language support." /> 157 158 159 160 161 <!-- OpenGraph --> 162 <meta property="og:type" content="website" /> 163 <meta property="og:site_name" content="Transcript.you" /> 164 <meta property="og:title" content="Get any Youtube Transcript | Transcript.you" /> 165 <meta property="og:description" content="Get the transcript for any YouTube video quickly and easily with our free tool. Supports multiple languages and clickable timestamps." /> 166 <meta property="og:url" content="https://transcript.you/tube" /> 167 <meta property="og:image" content="https://transcript.you/static/og/default.png" /> 168 <meta property="og:image:width" content="1200" /> 169 <meta property="og:image:height" content="630" /> 170 171 <!-- Twitter Card --> 172 <meta name="twitter:card" content="summary_large_image" /> 173 <meta name="twitter:site" content="@transcriptyou" /> 174 <meta name="twitter:title" content="Get any Youtube Transcript | Transcript.you" /> 175 <meta name="twitter:description" content="Get the transcript for any YouTube video quickly and easily with our free tool. Supports multiple languages and clickable timestamps." /> 176 <meta name="twitter:image" content="https://transcript.you/static/og/default.png" /> 177 178 179 180
180<script type="application/ld+json"> 181 { 182 "@context": "https://schema.org", 183 "@graph": [ 184 { 185 "@type": "Organization", 186 "name": "Transcript.you", 187 "url": "https://transcript.you", 188 "logo": "https://transcript.you/static/apple-touch-icon-180x180.png", 189 "description": "AI transcription platform for YouTube, TikTok, podcasts, lectures, and any audio or video file. 95%+ accuracy across 50+ languages.", 190 "knowsAbout": [ 191 "AI transcription", 192 "automatic speech recognition", 193 "ASR", 194 "OpenAI Whisper", 195 "audio transcription", 196 "video transcription", 197 "speech-to-text", 198 "voice-to-text", 199 "subtitle generation", 200 "SubRip SRT", 201 "WebVTT", 202 "speaker diarization", 203 "word error rate", 204 "podcast transcription", 205 "lecture transcription", 206 "meeting transcription", 207 "interview transcription", 208 "legal transcription", 209 "medical transcription", 210 "Zoom transcription", 211 "Google Meet transcription", 212 "Microsoft Teams transcription", 213 "MP3 transcription", 214 "WAV transcription", 215 "M4A transcription", 216 "MP4 video transcription", 217 "audio captioning", 218 "video captioning", 219 "closed captions", 220 "accessibility", 221 "AI summarization", 222 "blog post generation", 223 "show notes", 224 "translation" 225 ], 226 "sameAs": [] 227 228 }, 229 { 230 "@type": "WebSite", 231 "name": "Transcript.you", 232 "url": "https://transcript.you", 233 "potentialAction": { 234 "@type": "SearchAction", 235 "target": "https://transcript.you/tube?v={search_term_string}", 236 "query-input": "required name=search_term_string" 237 } 238 } 239 ] 240 } 241 </script>
241 242 243 <!-- Favicon --> 244 <!-- Favicon --> 245 <link rel="apple-touch-icon" sizes="57x57" href="/static/apple-touch-icon-57x57.png"> 246 <link rel="apple-touch-icon" sizes="60x60" href="/static/apple-touch-icon-60x60.png"> 247 <link rel="apple-touch-icon" sizes="72x72" href="/static/apple-touch-icon-72x72.png"> 248 <link rel="apple-touch-icon" sizes="76x76" href="/static/apple-touch-icon-76x76.png"> 249 <link rel="apple-touch-icon" sizes="114x114" 250 href="/static/apple-touch-icon-114x114.png"> 251 <link rel="apple-touch-icon" sizes="120x120" 252 href="/static/apple-touch-icon-120x120.png"> 253 <link rel="apple-touch-icon" sizes="144x144" 254 href="/static/apple-touch-icon-144x144.png"> 255 <link rel="apple-touch-icon" sizes="152x152" 256 href="/static/apple-touch-icon-152x152.png"> 257 <link rel="apple-touch-icon" sizes="167x167" 258 href="/static/apple-touch-icon-167x167.png"> 259 <link rel="apple-touch-icon" sizes="180x180" 260 href="/static/apple-touch-icon-180x180.png"> 261 <link rel="apple-touch-icon" sizes="1024x1024" 262 href="/static/apple-touch-icon-1024x1024.png"> 263 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"> 264 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> 265 <link rel="icon" type="image/png" sizes="48x48" href="/static/favicon-48x48.png"> 266 <link rel="icon" type="image/png" sizes="64x64" href="/static/favicon-64x64.png"> 267 <link rel="icon" type="image/png" sizes="128x128" href="/static/favicon-128x128.png"> 268 <link rel="icon" type="image/png" sizes="256x256" href="/static/favicon-256x256.png"> 269 <link rel="manifest" href="/static/site.webmanifest"> 270 <meta name="msapplication-TileImage" content="/static/mstile-144x144.png"> 271 272 <!-- Phosphor Icons CDN --> 273 <link rel="stylesheet" type="text/css" 274 href="https://cdn.jsdelivr.net/npm/@phosphor-icons/[email protected]/src/duotone/style.css" /> 275 276 <!-- DaisyUI and Tailwind CSS --> 277 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css" /> 278
278<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
278 279 280 <!-- Custom Fonts (Font Awesome for old icons) --> 281 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> 282 283<!-- Matomo -->
284<script> 285 var _paq = window._paq = window._paq || []; 286 /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ 287 _paq.push(['trackPageView']); 288 _paq.push(['enableLinkTracking']); 289 (function() { 290 var u="//tracking.transcript.you/"; 291 _paq.push(['setTrackerUrl', u+'matomo.php']); 292 _paq.push(['setSiteId', '']); 293 var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; 294 g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); 295 })(); 296</script>
296 297<!-- End Matomo Code --> 298</head> 299 300<body class="bg-gray-50" data-authenticated="false"> 301 <div class="drawer"> 302 <input id="my-drawer" type="checkbox" class="drawer-toggle" /> 303 <div class="drawer-content flex flex-col min-h-screen"> 304 <!-- Page content here --> 305 306 <!-- Desktop Bookmark Bar (hidden on /premium so pricing has full focus) --> 307 308 <section class="hidden sm:flex justify-center w-full px-4 py-2 text-center bg-black text-white"> 309 <div class="block mx-auto text-xs font-semibold max-w-fit"> 310 <svg xmlns="http://www.w3.org/2000/svg" class="inline w-5 h-5 fill-current" viewBox="0 0 256 256"> 311 <path 312 d="M208,144a80,80,0,0,1-160,0c0-30.57,14.42-58.26,31-80l33,32,26.27-72C159.86,41.92,208,88.15,208,144Z" 313 opacity="0.2"></path> 314 <path 315 d="M183.89,153.34a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68ZM216,144a88,88,0,0,1-176,0c0-27.92,11-56.47,32.66-84.85a8,8,0,0,1,11.93-.89l24.12,23.41,22-60.41a8,8,0,0,1,12.63-3.41C165.21,36,216,84.55,216,144Zm-16,0c0-46.09-35.79-85.92-58.21-106.33L119.52,98.74a8,8,0,0,1-13.09,3L80.06,76.16C64.09,99.21,56,122,56,144a72,72,0,0,0,144,0Z"> 316 </path> 317 </svg> 318 Finding this tool useful? Bookmark us 319 <span class="hidden macos"> 320 <kbd class="kbd text-base-content">cmd</kbd>+<kbd class="kbd text-base-content">d</kbd> 321 </span> 322 <span class="hidden window"> 323 <kbd class="kbd text-base-content">ctrl</kbd>+<kbd class="kbd text-base-content">d</kbd> 324 </span> 325 for easy and fast access! 326 </div> 327 </section> 328 329 330 <!-- Mobile Bookmark Alert (hidden on /premium) --> 331 332 <div id="mobile-bookmark-alert" 333 class="flex items-center justify-between w-full px-4 py-2 bg-black text-white text-xs sm:hidden"> 334 <div class="flex items-center"> 335 <i class="ph-duotone ph-star text-lg mr-2"></i> 336 <span class="font-semibold">Remember to bookmark us!</span> 337 </div> 338 <button class="btn btn-xs btn-ghost" onclick="this.parentElement.style.display='none'">â</button> 339 </div> 340 341 342 <nav class="navbar bg-base-100 border-b border-base-200 w-full max-w-full px-4 lg:px-8 relative z-50"> 343 <!-- navbar-start: flex-grow to take up available space --> 344 <div class="navbar-start flex-grow"> 345 <!-- Hamburger Menu Button --> 346 <label for="my-drawer" class="btn btn-ghost lg:hidden drawer-button" 347 style="padding-left:0rem !important; padding-right:0rem !important; "> 348 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" 349 stroke="currentColor"> 350 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 351 d="M4 6h16M4 12h8m-8 6h16" /> 352 </svg> 353 </label> 354 <!-- Logo --> 355 <a class="flex items-center text-xl normal-case" href="/"> 356 <svg xmlns="http://www.w3.org/2000/svg" class="hidden h-8 w-8 fill-blue-500 sm:inline"
357 viewBox="0 0 256 256"> 358 <path d="M232,64V192a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H224A8,8,0,0,1,232,64Z" 359 opacity="0.2"></path> 360 <path 361 d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM48,136a8,8,0,0,1,8-8H72a8,8,0,0,1,0,16H56A8,8,0,0,1,48,136Zm160,0a8,8,0,0,1-8,8H104a8,8,0,0,1,0-16h96A8,8,0,0,1,208,136Zm-48,32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h96A8,8,0,0,1,160,168Zm48,0a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16h16A8,8,0,0,1,208,168Z"> 362 </path> 363 </svg> 364 <span class="ml-2 font-bold text-gray-700">Transcript.you</span> 365 </a> 366 367 <!-- PRIMARY FUNCTIONS (LEFT) --> 368 369 <ul 370 class="menu menu-horizontal px-1 hidden lg:flex ml-4 gap-x-1 flex-nowrap whitespace-nowrap items-center"> 371 <!-- YOUTUBE DROPDOWN --> 372 <div class="dropdown dropdown-hover"> 373 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 374 <i class="ph-duotone ph-youtube-logo text-red-600"></i> 375 YouTube 376 <i class="ph-duotone ph-caret-down text-xs"></i> 377 </label> 378 <ul tabindex="0" 379 class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64"> 380 <li><a href="/tube">YouTube Transcript Generator</a></li> 381 <li><a href="/transcribe-youtube-videos">Transcribe 382 YouTube Videos</a></li> 383 <li><a href="/shorts">Shorts Transcript</a></li> 384 <li><a href="/video-to-text">Video to Text</a></li> 385 <li><a href="/generate-subtitles">Generate 386 Subtitles</a></li> 387 <li><a href="/video-to-srt">Video to SRT</a></li> 388 </ul> 389 </div> 390 391 <!-- AUDIO DROPDOWN --> 392 <div class="dropdown dropdown-hover"> 393 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 394 <i class="ph-duotone ph-microphone-stage text-purple-600"></i> 395 Audio 396 <i class="ph-duotone ph-caret-down text-xs"></i> 397 </label> 398 <ul tabindex="0" 399 class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64"> 400 <li><a href="/transcribe-audio">Transcribe Audio File</a></li> 401 <li><a href="/mp3-to-text">MP3 to Text</a></li> 402 <li><a href="/voice-to-text">Voice to Text</a></li> 403 <li><a href="/meeting-transcription">Meeting 404 Transcription</a></li> 405 <li><a href="/interview-transcription">Interview 406 Transcription</a></li> 407 <li><a href="/zoom-to-text">Zoom to Text</a></li> 408 </ul> 409 </div> 410 411 <!-- BULK TRANSCRIPTS DROPDOWN --> 412 <div class="dropdown dropdown-hover"> 413 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 414 <i class="ph-duotone ph-upload-simple text-green-600"></i> 415 Bulk 416 <i class="ph-duotone ph-caret-down text-xs"></i> 417 </label> 418 <ul tabindex="0" 419 class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-72"> 420 <li><a href="/bulk" class="font-bold"><i class="ph-duotone ph-stack text-indigo-600"></i> See all bulk sources</a></li> 421 <div class="divider my-0"></div> 422 <li><a href="/bulk-transcripts"><i class="ph-duotone ph-youtube-logo text-red-500"></i> Bulk YouTube Transcripts</a></li> 423 <li><a href="/bulk-tiktok"><i class="ph-duotone ph-tiktok-logo text-pink-500"></i> Bulk TikTok Transcripts</a></li> 424 <li><a href="/bulk-spotify"><i class="ph-duotone ph-spotify-logo text-green-500"></i> Bulk Spotify Podcasts</a></li> 425 <li><a href="/bulk-vimeo"><i class="ph-duotone ph-video text-sky-500"></i> Bulk Vimeo</a></li> 426 <li><a href="/bulk-twitch"><i class="ph-duotone ph-twitch-logo text-purple-600"></i> Bulk Twitch VODs</a></li> 427 <li><a href="/bulk-audio"><i class="ph-duotone ph-microphone text-emerald-500"></i> Bulk Audio Upload</a></li> 428 429 </ul> 430 </div> 431 432 <!-- TIKTOK DROPDOWN --> 433 <div class="dropdown dropdown-hover"> 434 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 435 <i class="ph-duotone ph-tiktok-logo text-slate-800"></i> 436 TikTok 437 <i class="ph-duotone ph-caret-down text-xs"></i> 438 </label> 439 <ul tabindex="0" 440 class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64"> 441 <li><a href="/tiktok" class="font-bold"><i class="ph-duotone ph-text-aa text-cyan-600"></i> TikTok Transcript</a></li> 442 <li><a href="/bulk-tiktok"><i class="ph-duotone ph-stack text-indigo-600"></i> Bulk TikTok Transcripts</a></li> 443 <li><a href="/tiktok-downloader"><i class="ph-duotone ph-download-simple text-pink-500"></i> TikTok Video Downloader</a></li> 444 </ul> 445 </div> 446 447 <!-- SUBTITLES DROPDOWN --> 448 <div class="dropdown dropdown-hover"> 449 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 450 <i class="ph-duotone ph-subtitles text-violet-600"></i> 451 Subtitles 452 <i class="ph-duotone ph-caret-down text-xs"></i> 453 </label> 454 <ul tabindex="0" 455 class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-72"> 456 <li><a href="/subtitles-and-captions" class="font-bold"><i class="ph-duotone ph-closed-captioning text-violet-600"></i> All Subtitle & Caption Tools</a></li> 457 <div class="divider my-0"></div> 458 <li><a href="/subtitle-downloader"><i class="ph-duotone ph-download-simple text-violet-500"></i> Download YouTube Subtitles</a></li> 459 <li><a href="/generate-subtitles"><i class="ph-duotone ph-magic-wand text-indigo-500"></i> Generate Subtitles</a></li> 460 <li><a href="/video-to-srt"><i class="ph-duotone ph-file-video text-blue-500"></i> Video to SRT</a></li> 461 <li><a href="/audio-to-srt"><i class="ph-duotone ph-file-audio text-purple-500"></i> Audio to SRT</a></li> 462 <li><a href="/video-to-vtt"><i class="ph-duotone ph-file-code text-sky-500"></i> Video to VTT</a></li> 463 <li><a href="/add-captions-to-video"><i class="ph-duotone ph-closed-captioning text-emerald-500"></i> Add Captions to Video</a></li> 464 <div class="divider my-0"></div> 465 <li><a href="/blog/how-to-download-youtube-subtitles"><i class="ph-duotone ph-book-open text-slate-500"></i> Guide: Download YouTube Subtitles</a></li> 466 </ul> 467 </div> 468 469 <li><a href="/files" class="px-3">Files <span 470 class="text-xs font-bold text-blue-600 align-top">(0)</span></a></li> 471 <li><a href="/refer" class="px-3 text-indigo-600 font-semibold shrink-0"> 472 <i class="ph-duotone ph-gift text-base align-text-bottom mr-1"></i> 473 Refer</a></li> 474 <li class="hidden xl:flex"><a href="/affiliate" class="px-3 text-emerald-600 font-semibold shrink-0"> 475 <i class="ph-duotone ph-handshake text-base align-text-bottom mr-1"></i> 476 Affiliate</a></li> 477 </ul> 478 </div> 479 <!-- START: MODIFIED SECTION --> 480 <!-- END: MODIFIED SECTION --> 481 <div class="navbar-end gap-2 w-auto flex-none"> 482 <!-- DOWNLOADERS DROPDOWN (BEFORE PRICING) --> 483 <div class="dropdown dropdown-hover hidden lg:block self-center mr-2"> 484 <label tabindex="0" class="btn btn-ghost btn-sm gap-1 px-3"> 485 <i class="ph-duotone ph-caret-down text-xs hidden xl:inline"></i>
486 <span class="hidden xl:inline">Downloaders</span> 487 <span class="inline xl:hidden text-2xl mb-1 leading-none">...</span> 488 </label> 489 <ul tabindex="0" 490 class="dropdown-content right-0 z-[2] menu p-2 shadow-2xl bg-white rounded-2xl w-56 border border-slate-100"> 491 <li class="menu-title px-4 py-2 text-xs opacity-50 uppercase tracking-widest font-bold"> 492 Media Downloaders</li> 493 <li><a href="/youtube-downloader" class="flex items-center gap-3 py-3"><i 494 class="ph-duotone ph-youtube-logo text-xl text-red-600"></i> YouTube 495 Downloader</a></li> 496 <li><a href="/facebook-downloader" class="flex items-center gap-3 py-3"><i 497 class="ph-duotone ph-facebook-logo text-xl text-blue-600"></i> Facebook 498 Downloader</a></li> 499 <li><a href="/tiktok-downloader" class="flex items-center gap-3 py-3"><i 500 class="ph-duotone ph-tiktok-logo text-xl text-slate-800"></i> TikTok 501 Downloader</a></li> 502 <li><a href="/instagram-downloader" class="flex items-center gap-3 py-3"><i 503 class="ph-duotone ph-instagram-logo text-xl text-pink-600"></i> Instagram 504 Downloader</a></li> 505 <li><a href="/x-downloader" class="flex items-center gap-3 py-3"><i 506 class="ph-duotone ph-x-logo text-xl text-slate-900"></i> Twitter (X) 507 Downloader</a></li> 508 <li><a href="/subtitle-downloader" class="flex items-center gap-3 py-3"><i 509 class="ph-duotone ph-subtitles text-xl text-violet-600"></i> Subtitle 510 Downloader</a></li> 511 </ul> 512 </div> 513 514 515 <!-- GUEST USER --> 516 <a href="/premium" 517 class="btn btn-ghost btn-sm hidden sm:inline-flex">Pricing</a> 518 519 <div class="tooltip tooltip-bottom" data-tip="Register"> 520 <a href="/register" 521 class="btn btn-ghost btn-sm sm:btn-warning sm:text-black sm:hover:bg-yellow-300 sm:hover:border-yellow-400"> 522 <i class="ph-duotone ph-user-plus text-2xl"></i> 523 <span class="hidden sm:inline sm:ml-1">Register</span> 524 </a> 525 </div> 526 <div class="tooltip tooltip-bottom" data-tip="Login"> 527 <a href="/login" 528 class="btn btn-ghost btn-sm sm:btn-primary sm:hover:bg-blue-500 sm:hover:border-blue-600"> 529 <i class="ph-duotone ph-sign-in text-2xl"></i> 530 <span class="hidden sm:inline sm:ml-1">Login</span> 531 </a> 532 </div> 533 534 </div> 535 </nav> 536 537 <!-- PROMOTION BAR (Configurable via app.py) --> 538 539 540 <main class="flex-grow w-full"> 541 542 543 544 545 546 547 548 549 550 551 552<div class="bg-blue-50 text-blue-900 py-2.5 px-4 text-center text-sm border-b border-blue-100"> 553 <div class="max-w-4xl mx-auto flex items-center justify-center gap-2"> 554 <i class="ph-fill ph-user-circle-plus text-lg"></i> 555 <p> 556 <strong>Don't lose your work!</strong> 557 <a href="/register" class="underline font-bold hover:text-blue-700">Sign Up / Login</a> 558 to permanently store and organize your transcriptions in your File Manager. 559 </p> 560 </div> 561</div> 562 563<section class="hero bg-blue-600 text-white" id="hero-form"> 564 <div class="hero-content text-center w-full py-10"> 565 <div class="w-full max-w-xl"> 566 567 568 <h1 class="text-4xl font-bold"> 569 Get a Youtube Transcript 570 </h1> 571 <p class="py-4"> 572 Enter YouTube Video ID or URL 573 below.
574 </p> 575 576 577 578 <form id="transcript-form" class="flex flex-col mx-auto sm:flex-row sm:justify-center" 579 action="/tube" 580 method="get"> 581 <input type="text" name="v" 582 placeholder="Paste YouTube URL or Video ID here..." 583 value="" 584 class="w-full mb-2 truncate input input-bordered input-rounded sm:w-sm sm:mb-0 sm:mr-2 text-black" 585 autocomplete="on" required /> 586 <button type="submit" 587 class="w-full btn btn-warning text-black btn-rounded sm:w-auto hover:bg-yellow-300 hover:border-yellow-400"> 588 <i class="ph-duotone ph-magnifying-glass text-lg mr-1"></i> 589 Get Transcript 590 </button> 591 </form> 592 </div> 593 </div> 594</section> 595 596<div class="w-full max-w-7xl mx-auto px-4 mt-6"> 597 <!-- USAGE BOX / GUEST & FREE USER PROMPT --> 598 599 <div class="w-full flex flex-col md:flex-row justify-between items-center gap-4 mb-6 px-2 py-2"> 600 601 <!-- LEFT: Usage Stats + Upgrade --> 602 603 604 <!-- RIGHT: Prompts / Actions --> 605 <div 606 class="flex flex-col md:flex-row items-center justify-center md:justify-end gap-3 w-full md:w-auto text-center md:text-right"> 607 608 <!-- GUEST VIEW --> 609 610 <div class="hidden md:block"> 611 <p class="text-sm text-base-content/80 font-medium">Don't lose your work!</p> 612 <p class="text-xs text-base-content/60">Permanently store and organize your transcriptions in your File 613 Manager.</p> 614 </div> 615 <!-- Mobile Only Text (Simplified) --> 616 <div class="block md:hidden mb-2"> 617 <p class="text-sm text-base-content/80 font-medium">Don't lose your work! Login to save.</p> 618 </div> 619 620 <a href="/register" class="btn btn-primary btn-sm text-white"> 621 Sign Up / Login 622 </a> 623 624 <!-- REGISTERED (FREE) USER VIEW --> 625 626 </div> 627 </div> 628 629</div> 630 631<article class="w-full py-8 bg-base-100"> 632 <div class="w-full max-w-7xl mx-auto px-4"> 633 634 635 636 <!-- START: NEW CONTENT FOR EMPTY PAGE (SPLIT BY TYPE) --> 637 638 <div class="w-full max-w-2xl mx-auto px-4 py-8"> 639 640 641 642 <div class="flex flex-col justify-center text-base-content"> 643 <h2 644 class="text-transform-none -rotate-1 text-2xl font-bold mb-4 inline-flex py-[0.9rem] px-4 justify-start w-fit rounded-box bg-blue-600 text-blue-50"> 645 How to Generate a YouTube Transcript ð¥ 646 </h2> 647 <ul class="mb-8 list-decimal list-inside space-y-2"> 648 <li>Paste the YouTube video link into the box above and click "Get Transcript."</li> 649 <li> 650 View the full transcript with clickable timestamps that sync with the video player. 651 </li> 652 <li> 653 Use the controls to switch languages or copy the text for your notes! 654 </li> 655 </ul> 656 657 <h2 658 class="text-transform-none -rotate-1 text-2xl font-bold mb-4 inline-flex py-[0.9rem] px-4 justify-start w-fit rounded-box bg-green-600 text-green-50"> 659 Why you need a YouTube Transcript? ð¤ 660 </h2> 661 <p class="mb-8"> 662 YouTube videos are invaluable for learning and entertainment. 663 However, sometimes you need to quickly find specific information, 664 <strong class="">take notes</strong>, or share the content with 665 someone who <strong class="">prefers reading</strong>. This tool 666 makes it a breeze to get the full text from any YouTube video. 667 </p> 668 669 <h3 670 class="text-transform-none -rotate-1 text-xl font-semibold mb-4 inline-flex py-[0.5rem] px-4 justify-start w-fit roun
670ded-box bg-yellow-500 text-black"> 671 For Note Takers & Students ð 672 </h3> 673 <p class="mb-8"> 674 It can be challenging to follow along with a video while taking 675 notes. With the transcript, you can easily 676 <strong class="">copy and paste</strong> the text into your notes. Use our AI-powered premium 677 features to instantly summarize the content or analyze its sentiment, saving you even more time. 678 </p> 679 680 <h3 681 class="text-transform-none -rotate-1 text-xl font-semibold mb-4 inline-flex py-[0.5rem] px-4 justify-start w-fit rounded-box bg-purple-600 text-purple-50"> 682 For Content Creators ð¬ 683 </h3> 684 <p class="mb-2"> 685 Content creators can use this tool to research and repurpose content effortlessly. 686 </p> 687 <ul class="mb-8 list-disc list-inside space-y-2"> 688 <li class=""> 689 <strong>Summarize the transcript</strong> and add it to the video description to <strong 690 class="">rank higher in search engines.</strong> 691 </li> 692 <li class=""> 693 Extract key points from the transcript to <strong class="">create engaging social media 694 posts</strong> that entice viewers to watch the full video. 695 </li> 696 <li class=""> 697 Use the transcript as a foundation for blog posts, articles, or detailed show notes related to 698 your video content. 699 </li> 700 </ul> 701 </div> 702 703 </div> 704 705 <!-- END: NEW CONTENT FOR EMPTY PAGE --> 706 707 708 709 </div> 710 711 <!-- SCROLL TO AI BUTTON --> 712 <button id="scroll-to-ai-btn" class="hidden fixed bottom-5 right-5 btn btn-primary btn-circle z-50 shadow-lg"> 713 <i class="ph-duotone ph-sparkle text-2xl"></i> 714 </button> 715 716 717 <!-- DOWNLOAD MODAL --> 718 <!-- DOWNLOAD MODAL --> 719 <dialog id="download_modal" class="modal"> 720 <div class="modal-box w-11/12 max-w-3xl"> 721 <div class="flex justify-between items-center mb-4"> 722 <h3 class="font-bold text-lg">Download Media</h3> 723 <form method="dialog"><button class="btn btn-sm btn-circle btn-ghost">â</button></form> 724 </div> 725 726 <div id="download_header_text"> 727 <p class="py-2 text-sm text-gray-500"> 728 Extracting all available formats (Audio, Video, HD) costs <span class="font-bold text-primary">2 729 Credits</span>. 730 </p> 731 </div> 732 733 <div id="download_loader" class="hidden flex flex-col items-center justify-center py-8"> 734 <span class="loading loading-spinner loading-lg text-warning"></span> 735 <p class="text-sm mt-2 text-gray-400">Fetching all formats via secure connection...</p> 736 </div> 737 738 <div id="download_options" class="overflow-x-auto mt-4"> 739 <!-- Table or Upsell will be injected here --> 740 </div> 741 </div> 742 </dialog> 743 744 745</article> 746 747<dialog id="auth_modal" class="modal"> 748 <div class="modal-box"> 749 <div class="text-center"> 750 <svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 mx-auto text-blue-500" viewBox="0 0 256 256"> 751 <path 752 d="M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-80,84a12,12,0,1,1,12-12A12,12,0,0,1,128,164Zm32-84H96V56a32,32,0,0,1,64,0Z" 753 opacity="0.2"></path> 754 <path 755 d="M208,72H168V56a40,40,0,0,0-80,0V72H48A24,24,0,0,0,24,96V208a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V96A24,24,0,0,0,208,72Zm-80,96a16,16,0,1,1,16-16A16,16,0,0,1,128,168ZM152,72H104V56a24,24,0,0,1,48,0Z"> 756 </path> 757 </svg> 758 <h3 id="auth-modal-title" class="mt-2 text-2xl font-bold">Unlock AI Features</h3> 759 <p id="auth-modal-text" class="py-4">To use our AI tools, please create a free account or log in.</p> 760 </div> 761 762 <div class="space-y-3 mt-4"> 763 <a href="/login/google" data-auth-link class="btn btn-block"> 764 <i class="fa-brands fa-google text-lg"></i> 765 Continue with Google 766 </a> 767 <a href="/login/microsoft" data-auth-link class="btn btn-block"> 768 <i class="fa-brands fa-microsoft text-lg"></i> 769 Continue with Microsoft 770 </a> 771 </div> 772 773 <div class="divider text-sm text-gray-500 font-medium my-4">OR</div> 774 775 <div class="flex gap-4"> 776 <a href="/login" data-auth-link class="btn flex-1"> 777 <i class="ph-duotone ph-sign-in text-lg"></i> 778 Login 779 </a> 780 <a href="/register" data-auth-link 781 class="btn btn-warning text-black flex-1 hover:bg-yellow-300 hover:border-yellow-400"> 782 <i class="ph-duotone ph-user-plus text-lg"></i> 783 Sign Up 784 </a> 785 </div> 786 </div> 787 <form method="dialog" class="modal-backdrop"> 788 <button>close</button> 789 </form> 790</dialog>
791<script> 792 // openAuthModal({title, text, next}) â the auth modal with its own wording 793 // and a return address. 794 // 795 // Several pages open this modal directly for AI features, so it has to stay 796 // correct for them: whatever a caller changes is put back when the modal 797 // closes. `next` is appended to all four links (both OAuth providers, login, 798 // register) so the account is created and the visitor lands back where they 799 // were, instead of on /premium with the transcript they came for gone. The 800 // server validates it with _safe_next, so a hostile value is simply dropped. 801 (function () { 802 const modal = document.getElementById('auth_modal'); 803 if (!modal) return; 804 const title = document.getElementById('auth-modal-title'); 805 const text = document.getElementById('auth-modal-text'); 806 const links = Array.prototype.slice.call(modal.querySelectorAll('[data-auth-link]')); 807 const defaults = { 808 title: title ? title.textContent : '', 809 text: text ? text.textContent : '', 810 hrefs: links.map(function (a) { return a.getAttribute('href'); }) 811 }; 812 813 function reset() { 814 if (title) title.textContent = defaults.title; 815 if (text) text.textContent = defaults.text; 816 links.forEach(function (a, i) { a.setAttribute('href', defaults.hrefs[i]); }); 817 } 818 modal.addEventListener('close', reset); 819 820 window.openAuthModal = function (opts) { 821 opts = opts || {}; 822 reset(); 823 if (opts.title && title) title.textContent = opts.title; 824 if (opts.text && text) text.textContent = opts.text; 825 if (opts.next) { 826 links.forEach(function (a, i) { 827 const base = defaults.hrefs[i]; 828 a.setAttribute('href', base + (base.indexOf('?') === -1 ? '?' : '&') + 829 'next=' + encodeURIComponent(opts.next)); 830 }); 831 } 832 try { modal.showModal(); } catch (_) { modal.setAttribute('open', ''); } 833 }; 834 })(); 835</script>
835 836 837<dialog id="confirm_email_modal" class="modal"> 838 <div class="modal-box"> 839 <div class="text-center"> 840 <i class="ph-duotone ph-envelope-simple text-5xl text-blue-500 mx-auto"></i> 841 <h3 class="mt-2 text-2xl font-bold">Please Confirm Your Email</h3> 842 <p class="py-2 text-sm">To use AI features on the free plan, you need to confirm your email address. A 843 confirmation link was sent to <strong>your 844 email</strong>.</p> 845 <p class="py-2 text-sm">Alternatively, <strong>upgrade to any premium plan</strong> to unlock all features 846 immediately, without email confirmation.</p> 847 848 <div id="resend-message" class="text-green-600 text-sm mt-2 font-semibold min-h-[1.25rem]"></div> 849 <button id="resend-confirm-btn" class="btn btn-link p-0 h-auto min-h-0 text-sm"> 850 <span class="btn-text">Resend confirmation email</span> 851 <span class="btn-loader hidden loading loading-spinner loading-xs ml-2"></span> 852 </button> 853 </div> 854 <div class="modal-action mt-4 grid grid-cols-2 gap-4"> 855 <form method="dialog" class="contents"> 856 <button class="btn w-full">Maybe Later</button> 857 </form> 858 <button id="upgrade-from-confirm-btn" 859 class="btn btn-warning text-black w-full flex-nowrap items-center justify-center"> 860 <i class="ph-duotone ph-crown"></i> 861 <span class="ml-2">Upgrade Now</span> 862 </button> 863 </div> 864 </div> 865 <form method="dialog" class="modal-backdrop"> 866 <button>close</button> 867 </form> 868</dialog> 869 870<dialog id="premium_required_modal" class="modal"> 871 <div class="modal-box"> 872 <div class="text-center"> 873 <i class="ph-duotone ph-rocket-launch text-5xl text-blue-500 mx-auto"></i> 874 <h3 class="mt-2 text-2xl font-bold">Upgrade to Unlock This Feature</h3> 875 <p class="py-2 text-sm">The <strong id="premium-feature-name"></strong> feature is available on our premium 876 plans. Upgrade now to get instant access to all AI tools.</p> 877 </div> 878 <div class="modal-action mt-4 grid grid-cols-2 gap-4"> 879 <form method="dialog" class="contents"> 880 <button class="btn w-full">Maybe Later</button> 881 </form> 882 <button id="upgrade-from-premium-modal-btn" 883 class="btn btn-warning text-black w-full flex-nowrap items-center justify-center"> 884 <i class="ph-duotone ph-crown"></i> 885 <span class="ml-2">Upgrade Now</span> 886 </button> 887 </div> 888 </div> 889 <form method="dialog" class="modal-backdrop"> 890 <button>close</button> 891 </form> 892</dialog> 893 894<dialog id="feedback_modal" class="modal"> 895 <div class="modal-box relative"> 896 <form method="dialog"> 897 <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">â</button> 898 </form> 899 <h3 class="font-bold text-lg">Submit Feedback</h3> 900 <form action="/submit-feedback" method="post" class="py-4"> 901 <div class="form-control"> 902 <label class="label"> 903 <span class="label-text">From</span> 904 </label> 905 <input type="text" name="from" placeholder="Your Name" class="input input-bordered" required> 906 </div> 907 <div class="form-control"> 908 <label class="label"> 909 <span class="label-text">Email</span> 910 </label> 911 <input type="email" name="email" placeholder="[email protected]" class="input input-bordered" required> 912 </div> 913 <div class="form-control"> 914 <label class="label"> 915 <span class="label-text">Feedback</span> 916 </label> 917 <textarea name="feedback" class="textarea textarea-bordered h-24" placeholder="Your feedback..." 918 required></textarea> 919 </div> 920 <div class="modal-action"> 921 <button type="submit" class="btn btn-primary">Submit</button> 922 <button type="button" class="btn" 923 onclick="document.getElementById('feedback_modal').close()">Close</button> 924 </div> 925 </form> 926 </div> 927 <form method="dialog" class="modal-backdrop"> 928 <button>close</button> 929 </form> 930</dialog> 931 932<!-- Refer & Earn Modal --> 933<dialog id="referral_modal" class="modal"> 934 <div class="modal-box max-w-md relative"> 935 <form method="dialog"> 936 <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">â</button> 937 </form> 938 <h3 class="font-bold text-2xl mb-2 flex items-center gap-2"> 939 <i class="ph-duotone ph-gift text-blue-600"></i> 940 Refer & Earn Pro 941 </h3> 942 <p class="py-4 text-gray-600">
942 Get 6 friends to sign up and get 1 month of <span 943 class="font-bold text-blue-600">Pro Plan</span> for free!</p> 944 945 <div class="bg-gray-50 p-4 rounded-xl border border-gray-200 mb-6"> 946 <label class="text-xs font-bold text-gray-500 uppercase mb-2 block">Your Unique Referral Link</label> 947 <div class="flex gap-2"> 948 <input type="text" readonly id="referral-link-input" 949 value="Login to get your link" 950 class="input input-bordered w-full text-sm font-mono bg-white"> 951 <button class="btn btn-primary" onclick="copyReferralLink()"> 952 <i class="ph-duotone ph-copy"></i> 953 </button> 954 </div> 955 <p id="copy-status" class="text-xs text-green-600 mt-2 hidden">Copied to clipboard!</p> 956 </div> 957 958 <div class="flex flex-col gap-3"> 959 <div class="flex items-center justify-between text-sm"> 960 <span class="text-gray-500">Your Referrals:</span> 961 <span class="font-bold text-blue-600">0/6</span> 962 </div> 963 <progress class="progress progress-primary w-full" 964 value="0" max="6"></progress> 965 </div> 966 967 <div class="modal-action"> 968 <form method="dialog"> 969 <button class="btn">Close</button> 970 </form> 971 </div> 972 </div> 973
973<script> 974 function copyReferralLink() { 975 const input = document.getElementById('referral-link-input'); 976 input.select(); 977 input.setSelectionRange(0, 99999); 978 navigator.clipboard.writeText(input.value); 979 980 const status = document.getElementById('copy-status'); 981 status.classList.remove('hidden'); 982 setTimeout(() => status.classList.add('hidden'), 2000); 983 } 984 </script>
984 985 <form method="dialog" class="modal-backdrop"> 986 <button>close</button> 987 </form> 988</dialog> 989 990 991<style> 992 /* Match premium.html's scroll-lock pattern: modal-BOX scrolls, 993 overscroll-behavior:contain stops scroll chaining to the page, 994 body locked via :has(). */ 995 dialog#transcript_checkout_modal[open] { 996 position: fixed; 997 top: 0; right: 0; bottom: 0; left: 0; 998 width: 100%; 999 height: 100dvh; 1000 max-width: 100%; 1001 max-height: 100dvh; 1002 margin: 0; 1003 padding: 0; 1004 border: 0; 1005 background: rgba(0, 0, 0, 0.5); 1006 display: block; 1007 overflow: hidden; 1008 box-sizing: border-box; 1009 } 1010 dialog#transcript_checkout_modal[open] .modal-box { 1011 position: absolute !important; 1012 top: 0 !important; 1013 right: 0 !important; 1014 bottom: 0 !important; 1015 left: 0 !important; 1016 width: auto !important; 1017 max-width: none !important; 1018 height: auto !important; 1019 max-height: none !important; 1020 margin: 0 !important; 1021 border-radius: 0 !important; 1022 overflow-y: auto; 1023 overscroll-behavior: contain; 1024 -webkit-overflow-scrolling: touch; 1025 } 1026 @media (min-width: 768px) { 1027 dialog#transcript_checkout_modal[open] .modal-box { 1028 position: relative !important; 1029 top: auto !important; 1030 right: auto !important; 1031 bottom: auto !important; 1032 left: auto !important; 1033 width: 100% !important; 1034 max-width: 32rem !important; 1035 height: auto !important; 1036 max-height: 90dvh !important; 1037 margin: 2rem auto !important; 1038 border-radius: 1rem !important; 1039 } 1040 dialog#transcript_checkout_modal[open] { 1041 display: flex; 1042 align-items: center; 1043 justify-content: center; 1044 } 1045 } 1046 body:has(dialog#transcript_checkout_modal[open]) { 1047 overflow: hidden !important; 1048 touch-action: none; 1049 } 1050</style> 1051<dialog id="transcript_checkout_modal" class="modal"> 1052 <div class="modal-box relative p-4 md:p-6"> 1053 <form method="dialog"> 1054 <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 z-10">â</button> 1055 </form> 1056 <div class="text-center mb-2 md:mb-4"> 1057 <i class="ph-duotone ph-rocket-launch text-3xl md:text-5xl text-blue-500 mx-auto"></i> 1058 <h3 class="mt-1 md:mt-2 text-lg md:text-2xl font-bold">Upgrade to Continue</h3> 1059 1060 1061 <div class="py-2 text-sm hidden md:block text-left"> 1062 <p class="mb-2"> 1063 You've used all your credits. Complete your purchase for the 1064 <strong>Creator Plan</strong> to: 1065 </p> 1066 1067 <ul class="space-y-1"> 1068 <li>â Unlock more AI features!</li> 1069 <li>â No ads!</li> 1070 <li>â Prioritized conversion</li> 1071 <li>â Download YouTube Videos</li> 1072 </ul> 1073 </div> 1074 1075 1076 </div> 1077 <div id="transcript-checkout"> 1078 <div class="flex justify-center items-center p-8"> 1079 <span class="loading loading-dots loading-lg"></span> 1080 </div> 1081 </div> 1082 </div> 1083 <form method="dialog" class="modal-backdrop"> 1084 <button>close</button> 1085 </form> 1086</dialog> 1087 1088 1089 1090<!-- NEW: Free Trial Modal --> 1091<dialog id="free_trial_modal" class="modal"> 1092 <div class="modal-box relative bg-white border-t-8 border-blue-600"> 1093 <form method="dialog"> 1094 <button class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4">â</button> 1095 </form> 1096 <h3 class="font-bold text-2xl text-center mb-2">You've hit your daily limit</h3> 1097 <p class="py-2 text-center text-gray-600"> 1098 Resets in <span class="font-bold text-red-600">24 Hours</span>. Try 1099 our Pro plan for FREE to continue. 1100 </p> 1101 1102 <div class="py-4"> 1103 <ul class="space-y-2 mb-6"> 1104 <li class="flex items-center"><i class="ph-duotone ph-check-circle text-green-500 mr-2"></i> Unlimited 1105 Transcripts</li> 1106 <li class="flex items-center"><i class="ph-duotone ph-check-circle text-green-500 mr-2"></i> Advanced AI 1107 Summaries</li> 1108 <li class="flex items-center"><i class="ph-duotone ph-check-circle text-green-500 mr-2"></i> 7 Days Free 1109 ($0.00 today)</li> 1110 </ul> 1111 1112 1113 <button type="button" 1114 class="btn btn-primary w-full text-lg shadow-lg hover:scale-105 transition-transform choose-plan-btn" 1115 data-plan-name="premium_yearly" data-trial="true"> 1116 Start My 7-Day Free Trial 1117 </button> 1118 1119 <div class="text-center mt-3"> 1120 <a href="/" class="link link-hover text-sm text-gray-500">No thanks, take me 1121 home</a> 1122 </div> 1123 </div> 1124 </div> 1125 <form method="dialog" class="modal-backdrop"> 1126 <button>close</button> 1127 </form> 1128</dialog> 1129 1130 1131 1132<!-- NEW: Pay Prompt Modal (Exceeding Limit) --> 1133<style> 1134 /* Always-visible scrollbar for the paywall form. 1135 Chrome/Safari hide overlay scrollbars until a scroll is already in 1136 progress, so nothing indicates the form continues below the fold. Forcing 1137 the track to render makes the remaining length obvious at a glance. */ 1138 #paywall-scroll { 1139 scrollbar-width: thin; /* Firefox */ 1140 scrollbar-color: #cbd5e1 #f1f5f9; 1141 scrollbar-gutter: stable; /* no reflow when it appears */ 1142 } 1143 #paywall-scroll::-webkit-scrollbar {
1144 width: 10px; 1145 -webkit-appearance: none; /* opt out of overlay style */ 1146 } 1147 #paywall-scroll::-webkit-scrollbar-track { 1148 background: #f1f5f9; 1149 border-radius: 8px; 1150 } 1151 #paywall-scroll::-webkit-scrollbar-thumb { 1152 background: #cbd5e1; 1153 border-radius: 8px; 1154 border: 2px solid #f1f5f9; 1155 } 1156 #paywall-scroll::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 1157</style> 1158 1159<dialog id="pay_prompt_modal" class="modal"> 1160 1161 <div class="modal-box relative bg-white border-t-8 border-yellow-400 p-0 overflow-hidden 1162 w-screen max-w-none h-[100dvh] max-h-[100dvh] rounded-none 1163 md:w-[92vw] md:max-w-[960px] md:h-[88vh] md:max-h-[880px] md:rounded-2xl"> 1164 <div class="flex flex-col md:flex-row h-full"> 1165 <!-- Left Side: Visuals & Benefits --> 1166 <div 1167 class="hidden md:flex md:w-2/5 bg-gradient-to-br from-blue-600 to-indigo-700 text-white p-8 flex-col justify-between"> 1168 <div> 1169 <h3 class="text-3xl font-bold mb-6">Unlimited Access</h3> 1170 <ul class="space-y-4"> 1171 <li class="flex items-start gap-3"> 1172 <i class="ph-duotone ph-check-circle text-2xl text-yellow-300"></i> 1173 <span class="text-lg">Unlimited Transcripts</span> 1174 </li> 1175 <li class="flex items-start gap-3"> 1176 <i class="ph-duotone ph-sparkle text-2xl text-yellow-300"></i> 1177 <span class="text-lg">Full AI Power (Summaries, Notes, etc.)</span> 1178 </li> 1179 <li class="flex items-start gap-3"> 1180 <i class="ph-duotone ph-clock text-2xl text-yellow-300"></i> 1181 <span class="text-lg">No Daily Wait Time</span> 1182 </li> 1183 <li class="flex items-start gap-3"> 1184 <i class="ph-duotone ph-file-arrow-down text-2xl text-yellow-300"></i> 1185 <span class="text-lg">SRT & VTT Export Support</span> 1186 </li> 1187 </ul> 1188 </div> 1189 <div class="text-sm opacity-80"> 1190 Trusted by 50,000+ creators and students. 1191 </div> 1192 </div> 1193 1194 <!-- Right Side: compact fixed header, scrolling form, thin footer --> 1195 <div class="flex-1 flex flex-col min-h-0"> 1196 1197 1198 1199 <div class="shrink-0 px-4 md:px-8 pt-3 md:pt-5 pb-3 border-b border-gray-100"> 1200 <div class="flex items-center justify-between gap-2"> 1201 1202 <h2 id="paywall-headline" 1203 class="text-lg md:text-2xl font-extrabold text-gray-900 whitespace-nowrap"> 1204 You've hit your daily limit! 1205 </h2> 1206 1207 <form method="dialog" class="shrink-0"> 1208 <button aria-label="Close" 1209 class="btn btn-sm md:btn-md btn-circle bg-gray-100 hover:bg-gray-200 1210 border-0 text-gray-600 text-base md:text-lg shadow-sm">â</button> 1211 </form> 1212 </div> 1213 </div> 1214 1215 1216 <div id="paywall-scroll" class="relative flex-1 overflow-y-auto px-4 md:px-8 py-3" 1217 style="-webkit-overflow-scrolling: touch;"> 1218 1219 <div id="paywall-sb" class="pointer-events-none absolute top-2 right-1 bottom-2 w-1.5 1220 rounded-full bg-gray-200/70 opacity-0 transition-opacity"> 1221 <div id="paywall-sb-thumb" 1222 class="w-full rounded-full bg-gray-400/80"></div> 1223 </div> 1224 1225 <div id="paywall-cta" class="py-6"> 1226 1227 1228 <a id="pay-prompt-signup" href="/register" 1229 class="btn btn-warning btn-lg btn-block text-black shadow-xl 1230 hover:scale-105 transition-all duration-200 py-4 h-auto text-xl"> 1231 <i class="ph-duotone ph-user-plus text-2xl mr-2"></i> 1232 Create Free Account 1233 </a> 1234 <p class="mt-3 text-sm text-gray-400"> 1235 Sign up in seconds â then unlock a 7-day free trial 1236 </p> 1237 1238 <a href="/premium" 1239 class="link link-hover text-gray-400 text-xs mt-2 inline-block">Compare all plans</a> 1240 </div> 1241 1242 <div id="paywall-checkout" class="w-full min-h-[300px] hidden" 1243 data-plan-name="premium_yearly" data-trial="true" 1244 data-upgrade-cta="paywall_guest"> 1245 <div class="flex justify-center items-center p-8">
1246 <span class="loading loading-dots loading-lg"></span> 1247 </div> 1248 </div> 1249 </div> 1250 1251 1252 </div> 1253 </div> 1254 </div> 1255 <form method="dialog" class="modal-backdrop"> 1256 <button>close</button> 1257 </form> 1258</dialog> 1259 1260<!-- CHECKOUT MODAL (Global) --> 1261 1262<style> 1263 dialog#checkout_modal[open] { 1264 position: fixed; 1265 top: 0; right: 0; bottom: 0; left: 0; 1266 width: 100%; 1267 height: 100dvh; 1268 max-width: 100%; 1269 max-height: 100dvh; 1270 margin: 0; 1271 padding: 0; 1272 border: 0; 1273 background: rgba(0, 0, 0, 0.5); 1274 display: block; 1275 overflow: hidden; 1276 box-sizing: border-box; 1277 } 1278 /* Absolute-positioned modal-box covers the full dialog edge-to-edge 1279 on mobile, side-stepping flex sizing where DaisyUI's .modal-box 1280 won the width race even with !important. */ 1281 dialog#checkout_modal[open] .modal-box { 1282 position: absolute !important; 1283 top: 0 !important; 1284 right: 0 !important; 1285 bottom: 0 !important; 1286 left: 0 !important; 1287 width: auto !important; 1288 max-width: none !important; 1289 height: auto !important; 1290 max-height: none !important; 1291 margin: 0 !important; 1292 border-radius: 0 !important; 1293 overflow-y: auto; 1294 overscroll-behavior: contain; 1295 -webkit-overflow-scrolling: touch; 1296 } 1297 @media (min-width: 768px) { 1298 dialog#checkout_modal[open] .modal-box { 1299 position: relative !important; 1300 top: auto !important; 1301 right: auto !important; 1302 bottom: auto !important; 1303 left: auto !important; 1304 width: 100% !important; 1305 max-width: 32rem !important; 1306 height: auto !important; 1307 max-height: 90dvh !important; 1308 margin: 2rem auto !important; 1309 border-radius: 1rem !important; 1310 } 1311 dialog#checkout_modal[open] { 1312 display: flex; 1313 align-items: center; 1314 justify-content: center; 1315 } 1316 } 1317</style> 1318<dialog id="checkout_modal" class="modal"> 1319 <div class="modal-box p-0 relative"> 1320 <form method="dialog"> 1321 <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 z-50">â</button> 1322 </form> 1323 <div id="checkout" style="min-height: 50dvh;"> 1324 <div class="flex justify-center items-center p-8"> 1325 <span class="loading loading-dots loading-lg"></span> 1326 </div> 1327 </div> 1328 </div> 1329 <form method="dialog" class="modal-backdrop"> 1330 <button>close</button> 1331 </form> 1332</dialog> 1333
1334<script> 1335 // Defined here, at the end of the partial, because it needs #pay_prompt_modal 1336 // to exist: the auth-modal script above runs while the page is still parsing 1337 // and that dialog is declared several hundred lines below it, so a copy of 1338 // this living up there found null, returned early, and left window.openPaywall 1339 // undefined â every caller silently fell back to the daily-limit wording. 1340 // openPaywall({title, cta}) â the same paywall, worded for the thing the 1341 // visitor just tried to do, and tagged so each trigger is measurable 1342 // separately. "You've hit your daily limit!" is wrong copy for someone 1343 // who clicked SRT or just finished a download. Everything is restored on 1344 // close, because the daily-limit path opens this same modal. 1345 (function () { 1346 const pw = document.getElementById('pay_prompt_modal'); 1347 if (!pw) return; 1348 const head = document.getElementById('paywall-headline'); 1349 const mount = document.getElementById('paywall-checkout'); 1350 const defaults = { 1351 title: head ? head.textContent.trim() : '', 1352 cta: mount ? mount.getAttribute('data-upgrade-cta') : null 1353 }; 1354 pw.addEventListener('close', function () { 1355 if (head) head.textContent = defaults.title; 1356 if (mount && defaults.cta) mount.setAttribute('data-upgrade-cta', defaults.cta); 1357 }); 1358 window.openPaywall = function (opts) { 1359 opts = opts || {}; 1360 if (opts.title && head) head.textContent = opts.title; 1361 if (opts.cta && mount) mount.setAttribute('data-upgrade-cta', opts.cta); 1362 try { pw.showModal(); } catch (_) { pw.setAttribute('open', ''); } 1363 }; 1364 })(); 1365 1366</script>
1366 1367 1368<!-- templates/partials/_stripe_logic.html --> 1369<!-- Stripe.js v3 -->
1370<script src="https://js.stripe.com/v3/"></script>
1370 1371
1372<script> 1373 (function () { 1374 const stripe = Stripe("pk_live_51GYV9XIHI17dZ9P7DQ88NMUhg7MBvq3VIhBNOxMw1v4a3HOPk0N92IdHXeHvsnQO48BAdyFA2fTtJewjf5pqyabu0071HXg26E"); 1375 const checkoutModal = document.getElementById('checkout_modal'); 1376 let checkout = null; // To hold the Stripe Checkout instance 1377 1378 // ------------------------------------------------------------------ 1379 // Session warming. 1380 // 1381 // Measured breakdown of click -> pay-button-visible: the POST to 1382 // /create-checkout-session accounts for 1.4-1.8s of it, because that 1383 // endpoint makes two sequential Stripe API calls (the embedded session 1384 // plus its hosted fallback). None of that work depends on the click â 1385 // only on which plan is being bought â so it can start on the first 1386 // real signal of intent and be finished, or nearly, by the time the 1387 // click lands. 1388 // 1389 // Intent is hover-with-dwell on desktop (a bare mouseenter fires on 1390 // every button the pointer crosses while scrolling), touchstart on 1391 // mobile, and focus for keyboard users. 1392 // 1393 // The cost is the same trade the paywall below already makes: sessions 1394 // are created for people who hover and never buy. Bounded by caching 1395 // one in-flight promise per plan per pageview, so a user who hovers the 1396 // same plan ten times still creates exactly one. 1397 // ------------------------------------------------------------------ 1398 // Fire-and-forget beacon so we learn how often embedded checkout is 1399 // actually blocked. sendBeacon specifically, not fetch: the blocked path 1400 // navigates to the hosted URL on the very next line, and a normal fetch 1401 // would be cancelled by that navigation before it left the browser. 1402 function checkoutBeacon(event, surface, reason) { 1403 try { 1404 const body = JSON.stringify({ 1405 event: event, surface: surface, reason: String(reason || '').slice(0, 120) 1406 }); 1407 if (navigator.sendBeacon) { 1408 navigator.sendBeacon('/checkout-telemetry', 1409 new Blob([body], { type: 'application/json' })); 1410 } else { 1411 fetch('/checkout-telemetry', { 1412 method: 'POST', headers: { 'Content-Type': 'application/json' }, 1413 body: body, keepalive: true 1414 }).catch(function () {}); 1415 } 1416 } catch (_) {} 1417 } 1418 1419 const HOVER_DWELL_MS = 80; 1420 const WARM_TTL_MS = 10 * 60 * 1000; // don't reuse a session forever 1421 const warmCache = new Map(); // key -> {promise, at} 1422 const hoverTimers = new WeakMap(); 1423 1424 function warmKey(planName, isTrial) { return planName + '|' + (isTrial ? 't' : 'f'); } 1425 1426 // `warm` marks a session created speculatively on hover rather than by 1427 // a click, so the server can count how many of these are never used. 1428 function requestSession(planName, isTrial, warm) { 1429 return fetch('/create-checkout-session', { 1430 method: 'POST', 1431 headers: { 'Content-Type': 'application/json' }, 1432 body: JSON.stringify({ 1433 planName: planName, 1434 returnUrl: window.location.href, 1435 isTrial: isTrial, 1436 warm: !!warm 1437 }) 1438 }).then(r => r.json()); 1439 } 1440 1441 function warmSession(planName, isTrial) { 1442 if (!planName) return null; 1443 const key = warmKey(planName, isTrial); 1444 const hit = warmCache.get(key); 1445 if (hit && (Date.now() - hit.at) < WARM_TTL_MS) return hit.promise; 1446 1447 // A rejected or error-carrying warm must not be cached as the 1448 // answer â drop it so the click retries cleanly rather than 1449 // inheriting a stale failure. 1450 const promise = requestSession(planName, isTrial, true) 1451 .then(payload => { 1452 if (!payload || payload.error || !payload.clientSecret) warmCache.delete(key); 1453 return payload; 1454 }) 1455 .catch(e => { warmCache.delete(key); throw e; }); 1456 1457 warmCache.set(key, { promise: promise, at: Date.now() }); 1458 return promise; 1459 } 1460 1461 function warmFromButton(btn) { 1462 if (!btn || btn.classList.contains('cursor-default')) return; 1463 warmSession(btn.dataset.planName, btn.dataset.trial === 'true'); 1464 } 1465 1466 function attachWarmHandlers(btn) { 1467 btn.addEventListener('mouseenter', () => { 1468 clearTimeout(hoverTimers.get(btn)); 1469 hoverTimers.set(btn, setTimeout(() => warmFromButton(btn), HOVER_DWELL_MS)); 1470 }); 1471 btn.addEventListener('mouseleave', () => clearTimeout(hoverTimers.get(btn)));
1472 // Touch and keyboard are already deliberate; no dwell needed. 1473 btn.addEventListener('touchstart', () => warmFromButton(btn), { passive: true }); 1474 btn.addEventListener('focus', () => warmFromButton(btn)); 1475 } 1476 1477 // Helper function for checkout 1478 async function handleCheckout(btn) { 1479 if (btn.classList.contains('cursor-default')) return; // Current plan 1480 1481 const planName = btn.dataset.planName; 1482 const isTrial = btn.dataset.trial === 'true'; 1483 1484 // Tracking â fire BEFORE we open the modal so we capture intent 1485 // even if the user bounces while Stripe is loading. 1486 try { 1487 if (window.__trackUpgradeOpen) { 1488 window.__trackUpgradeOpen( 1489 btn.dataset.upgradeCta || 'premium_choose_plan', 1490 {plan_name: planName, is_trial: isTrial} 1491 ); 1492 } 1493 } catch (_) {} 1494 1495 if (checkoutModal) { 1496 checkoutModal.showModal(); 1497 } else { 1498 console.error("Checkout modal not found."); 1499 return; 1500 } 1501 1502 let payload; 1503 try { 1504 // Started on hover/touch/focus, so this usually resolves 1505 // immediately. warmSession() returns the in-flight promise when 1506 // the request is still running, so a fast click waits for the 1507 // same request instead of firing a second one. 1508 payload = await (warmSession(planName, isTrial) || requestSession(planName, isTrial, false)); 1509 } catch (e) { 1510 const checkoutDiv = document.getElementById('checkout'); 1511 if (checkoutDiv) { 1512 checkoutDiv.innerHTML = `<p class="text-red-500 p-8 text-center">Network error contacting our server.</p>`; 1513 } 1514 return; 1515 } 1516 1517 const { clientSecret, fallbackUrl, error } = payload; 1518 1519 if (error || !clientSecret) { 1520 if (fallbackUrl) { window.location.href = fallbackUrl; return; } 1521 const checkoutDiv = document.getElementById('checkout'); 1522 if (checkoutDiv) { 1523 checkoutDiv.innerHTML = `<p class="text-red-500 p-8 text-center">Could not initialize payment. ${error || ''}</p>`; 1524 } 1525 return; 1526 } 1527 1528 if (checkout) { 1529 checkout.destroy(); 1530 } 1531 1532 try { 1533 checkout = await stripe.initEmbeddedCheckout({ clientSecret }); 1534 const checkoutDiv = document.getElementById('checkout'); 1535 if (checkoutDiv) { 1536 checkoutDiv.innerHTML = ''; 1537 checkout.mount('#checkout'); 1538 } 1539 checkoutBeacon('embedded_ok', 'modal'); 1540 } catch (e) { 1541 checkoutBeacon('embedded_blocked', 'modal', e && e.message); 1542 // Embedded init failed (almost always tracking-protection 1543 // blocking m.stripe.network). Send the user to the hosted 1544 // checkout instead of leaving them with a broken modal. 1545 console.warn('Embedded init failed, falling back to hosted:', e); 1546 if (fallbackUrl) { 1547 window.location.href = fallbackUrl; 1548 } else { 1549 const checkoutDiv = document.getElementById('checkout'); 1550 if (checkoutDiv) { 1551 checkoutDiv.innerHTML = `<p class="text-red-500 p-8 text-center">Could not load checkout. ${e.message || ''}</p>`; 1552 } 1553 } 1554 } 1555 } 1556 1557 // Handle button clicks directly 1558 function attachCheckoutHandlers() { 1559 document.querySelectorAll('.choose-plan-btn').forEach(button => { 1560 // Remove old listeners if any (simple way: clone node) 1561 const newButton = button.cloneNode(true); 1562 button.parentNode.replaceChild(newButton, button); 1563 1564 newButton.addEventListener('click', (event) => { 1565 event.preventDefault(); 1566 event.stopPropagation(); 1567 handleCheckout(event.currentTarget); 1568 }); 1569 attachWarmHandlers(newButton); 1570 }); 1571 1572 // Handle entire card clicks if present (on premium.html) 1573 document.querySelectorAll('.plan-card').forEach(card => { 1574 card.addEventListener('mouseenter', () => { 1575 clearTimeout(hoverTimers.get(card)); 1576 hoverTimers.set(card, setTimeout( 1577 () => warmFromButton(card.querySelector('.choose-plan-btn')), HOVER_DWELL_MS)); 1578 }); 1579 card.addEventListener('mouseleave', () => clearTimeout(hoverTimers.get(card))); 1580 1581 card.addEventListener('click', (event) => { 1582 const button = card.querySelector('.choose-plan-btn'); 1583 const link = card.querySelector('a.btn'); 1584 1585 if (button) { 1586 handleCheckout(button); 1587 } else if (link) { 1588 link.click(); 1589 } 1590 }); 1591 }); 1592 } 1593 1594 // Initial attachment 1595 if (document.readyState === "loading") { 1596 document.addEventListener("DOMContentLoaded", attachCheckoutHandlers); 1597 } else { 1598 attachCheckoutHandlers(); 1599 } 1600 1601 // Also expose it to window for dynamic elements 1602 window.attachCheckoutHandlers = attachCheckoutHandlers; 1603 1604 // ------------------------------------------------------------------ 1605 // Paywall: mount the embedded form directly, no button, no 2nd modal. 1606 // 1607 // The old flow was paywall modal -> click -> spinner -> a SECOND dialog 1608 // stacked on top. Roughly 30% of members dropped at that button 1609 // (2,309 paywall hits -> 1,628 clicks), and for guests it was a brand 1610 // new step they never used to face. Now the limit message and the card 1611 // form are one surface. 1612 // 1613 // Cost of doing it this way: a checkout session (plus its hosted 1614 // fallback) is created on every paywall view rather than per click, so 1615 // session volume tracks paywall hits. That is the deliberate trade. 1616 // ------------------------------------------------------------------ 1617 const paywallModal = document.getElementById('pay_prompt_modal'); 1618 const paywallMount = document.getElementById('paywall-checkout'); 1619 let paywallLoaded = false;
1620 1621 async function loadPaywallCheckout() { 1622 if (paywallLoaded || !paywallMount) return; 1623 paywallLoaded = true; // set first: an observer can fire twice 1624 1625 const planName = paywallMount.dataset.planName || 'premium_yearly'; 1626 const isTrial = paywallMount.dataset.trial === 'true'; 1627 1628 try { 1629 if (window.__trackUpgradeOpen) { 1630 window.__trackUpgradeOpen( 1631 paywallMount.dataset.upgradeCta || 'paywall_inline', 1632 {plan_name: planName, is_trial: isTrial, inline: true} 1633 ); 1634 } 1635 } catch (_) {} 1636 1637 let payload; 1638 try { 1639 const response = await fetch('/create-checkout-session', { 1640 method: 'POST', 1641 headers: {'Content-Type': 'application/json'}, 1642 body: JSON.stringify({ 1643 planName: planName, 1644 returnUrl: window.location.href, 1645 isTrial: isTrial 1646 }) 1647 }); 1648 payload = await response.json(); 1649 } catch (e) { 1650 paywallMount.innerHTML = 1651 '<p class="text-red-500 p-6 text-center">Network error â please retry.</p>'; 1652 paywallLoaded = false; // allow a retry on reopen 1653 return; 1654 } 1655 1656 const {clientSecret, fallbackUrl, error} = payload || {}; 1657 if (error || !clientSecret) { 1658 // Already subscribed, invalid plan, or Stripe refused. 1659 paywallMount.innerHTML = fallbackUrl 1660 ? '<a class="btn btn-warning btn-block text-black" href="' + fallbackUrl + '">Continue to checkout</a>' 1661 : '<p class="text-red-500 p-6 text-center">' + (error || 'Could not start checkout.') + '</p>'; 1662 return; 1663 } 1664 1665 try { 1666 if (checkout) { checkout.destroy(); checkout = null; } 1667 checkout = await stripe.initEmbeddedCheckout({clientSecret}); 1668 paywallMount.innerHTML = ''; 1669 checkout.mount('#paywall-checkout'); 1670 checkoutBeacon('embedded_ok', 'paywall'); 1671 } catch (e) { 1672 checkoutBeacon('embedded_blocked', 'paywall', e && e.message); 1673 // Embedded blocked (tracking protection / adblock). Offer the 1674 // hosted page rather than leaving an empty box. 1675 console.warn('Embedded init failed, offering hosted:', e); 1676 paywallMount.innerHTML = fallbackUrl 1677 ? '<a class="btn btn-warning btn-block text-black" href="' + fallbackUrl + '">Continue to secure checkout</a>' 1678 : '<p class="text-red-500 p-6 text-center">Could not load checkout.</p>'; 1679 } 1680 } 1681 1682 // Trigger on the button, not on the modal opening. Auto-loading the 1683 // form for everyone who hit a limit removed the intent filter and 1684 // measurably cost conversions; the click is the signal. 1685 const paywallStartBtn = document.getElementById('paywall-start-btn'); 1686 const paywallCta = document.getElementById('paywall-cta'); 1687 if (paywallStartBtn && paywallMount) { 1688 paywallStartBtn.addEventListener('click', function () { 1689 // Swap the CTA for the form in place â one modal, no stacking. 1690 if (paywallCta) paywallCta.classList.add('hidden'); 1691 paywallMount.classList.remove('hidden'); 1692 loadPaywallCheckout(); 1693 }); 1694 } 1695 // Reset when the modal closes so reopening shows the CTA again rather 1696 // than a stale form bound to an expired session. 1697 if (paywallModal) { 1698 paywallModal.addEventListener('close', () => { 1699 if (paywallCta) paywallCta.classList.remove('hidden'); 1700 if (paywallMount) paywallMount.classList.add('hidden'); 1701 paywallLoaded = false; 1702 if (checkout) { try { checkout.destroy(); } catch (_) {} checkout = null; } 1703 paywallMount.innerHTML = 1704 '<div class="flex justify-center items-center p-8">' 1705 + '<span class="loading loading-dots loading-lg"></span></div>'; 1706 }); 1707 } 1708 1709 // ------------------------------------------------------------------ 1710 // "Did they actually engage with the payment form?" 1711 // 1712 // Stripe's embed is a cross-origin iframe, so field-level events are 1713 // invisible to us â we cannot see which input was focused, or typed in.
1714 // What IS observable: when focus moves INTO that iframe the parent 1715 // window fires blur and document.activeElement becomes the iframe. That 1716 // is a reliable proxy for "clicked into the card form", which turns 1717 // shown -> paid 1718 // into 1719 // shown -> engaged -> paid 1720 // so an abandonment can be attributed to the offer or to the form. 1721 // Fires once per page view. 1722 // ------------------------------------------------------------------ 1723 (function () { 1724 let sent = false; 1725 window.addEventListener('blur', function () { 1726 if (sent) return; 1727 const el = document.activeElement; 1728 if (!el || el.tagName !== 'IFRAME') return; 1729 if (!/stripe/i.test(el.name || '') && !/stripe/i.test(el.src || '')) return; 1730 const modal = document.getElementById('pay_prompt_modal'); 1731 if (modal && !modal.hasAttribute('open')) return; 1732 sent = true; 1733 try { 1734 fetch('/api/track', { 1735 method: 'POST', credentials: 'same-origin', 1736 headers: {'Content-Type': 'application/json'}, 1737 body: JSON.stringify({event: 'checkout_form_engaged', meta: { 1738 surface: (document.getElementById('paywall-checkout') || {}).dataset 1739 ? (document.getElementById('paywall-checkout').dataset.upgradeCta || 'paywall') 1740 : 'paywall' 1741 }}) 1742 }); 1743 } catch (_) {} 1744 }); 1745 })(); 1746 1747 // ------------------------------------------------------------------ 1748 // Drive the custom scrollbar. Native ones are overlay-only on macOS/iOS, 1749 // so there is nothing to style â this reflects scroll position directly. 1750 // ------------------------------------------------------------------ 1751 (function () { 1752 const sc = document.getElementById('paywall-scroll'); 1753 const bar = document.getElementById('paywall-sb'); 1754 const thumb = document.getElementById('paywall-sb-thumb'); 1755 if (!sc || !bar || !thumb) return; 1756 1757 function sync() { 1758 const overflow = sc.scrollHeight - sc.clientHeight; 1759 if (overflow <= 2) { bar.style.opacity = '0'; return; } 1760 bar.style.opacity = '1'; 1761 const trackH = bar.clientHeight; 1762 // Floor the thumb so it stays grabbable on very long forms. 1763 const thumbH = Math.max(28, trackH * (sc.clientHeight / sc.scrollHeight)); 1764 const maxTravel = trackH - thumbH; 1765 thumb.style.height = thumbH + 'px'; 1766 thumb.style.transform = 1767 'translateY(' + (maxTravel * (sc.scrollTop / overflow)) + 'px)'; 1768 } 1769 1770 sc.addEventListener('scroll', sync, {passive: true}); 1771 window.addEventListener('resize', sync); 1772 // Stripe mounts asynchronously and then resizes its iframe, so the 1773 // content height changes after load â watch for it rather than 1774 // measuring once. 1775 if (window.ResizeObserver) { 1776 new ResizeObserver(sync).observe(sc.firstElementChild ? sc : sc); 1777 const mount = document.getElementById('paywall-checkout'); 1778 if (mount) new ResizeObserver(sync).observe(mount); 1779 } 1780 setTimeout(sync, 500); setTimeout(sync, 2000); setTimeout(sync, 5000); 1781 })(); 1782 1783 // Reset modal content when it's closed 1784 if (checkoutModal) { 1785 checkoutModal.addEventListener('close', () => { 1786 if (checkout) { 1787 checkout.destroy(); 1788 checkout = null; 1789 } 1790 const checkoutDiv = document.getElementById('checkout'); 1791 if (checkoutDiv) { 1792 checkoutDiv.innerHTML = '<div class="flex justify-center items-center p-8"><span class="loading loading-dots loading-lg"></span></div>'; 1793 } 1794 }); 1795 } 1796 })(); 1797</script>
1797 1798 1799 1800<!-- Logic to trigger the trial/pay modals if limits are hit -->
1801<script> 1802 (function () { 1803 function openAutomodal() { 1804 1805 } 1806 1807 if (document.readyState === "loading") { 1808 document.addEventListener("DOMContentLoaded", openAutomodal); 1809 } else { 1810 openAutomodal(); 1811 } 1812 })(); 1813</script>
1813 1814 1815 1816 1817 1818 1819 1820<style> 1821 #transcript-content::-webkit-scrollbar, 1822 #ai-section::-webkit-scrollbar { 1823 width: 8px; 1824 } 1825 1826 #transcript-content::-webkit-scrollbar-track, 1827 #ai-section::-webkit-scrollbar-track { 1828 background: #f1f1f1; 1829 border-radius: 10px; 1830 } 1831 1832 #transcript-content::-webkit-scrollbar-thumb, 1833 #ai-section::-webkit-scrollbar-thumb { 1834 background: #d1d5db; 1835 border-radius: 10px; 1836 } 1837 1838 #transcript-content::-webkit-scrollbar-thumb:hover, 1839 #ai-section::-webkit-scrollbar-thumb:hover { 1840 background: #9ca3af; 1841 } 1842 1843 .rotate-180 { 1844 transform: rotate(180deg); 1845 } 1846 1847 .pinned { 1848 position: fixed; 1849 top: 1rem; 1850 width: 55%; 1851 z-index: 50; 1852 } 1853 1854 /* STYLES FOR GENERATED CONTENT SCROLL & HEIGHT LIMIT */ 1855 .content-area { 1856 max-height: 75vh; 1857 overflow-y: auto; 1858 scrollbar-width: thin; 1859 scrollbar-color: #d1d5db #f1f1f1; 1860 } 1861 1862 .content-area::-webkit-scrollbar { 1863 width: 8px; 1864 } 1865 1866 .content-area::-webkit-scrollbar-track { 1867 background: #f1f1f1; 1868 border-radius: 10px; 1869 } 1870 1871 .content-area::-webkit-scrollbar-thumb { 1872 background: #d1d5db; 1873 border-radius: 10px; 1874 } 1875 1876 .content-area::-webkit-scrollbar-thumb:hover { 1877 background: #9ca3af; 1878 } 1879 1880 @media (max-width: 767px) { 1881 .content-area { 1882 max-height: 90vh; 1883 } 1884 } 1885</style> 1886 1887 1888
1889<script> 1890 const AI_FEATURES = {"ai_content_checker": {"credit_cost": 1, "display_name": "AI Checker", "is_premium": true, "prompt": "Analyze the text and provide an assessment of the likelihood that it was generated by AI."}, "blog_article": {"credit_cost": 5, "display_name": "Blog Article", "is_premium": true, "prompt": "Write a short blog article based on the provided transcript, with a title, introduction, body, and conclusion."}, "blog_outline": {"credit_cost": 2, "display_name": "Blog Outline", "is_premium": false, "prompt": "Generate a structured outline for a blog post based on the transcript."}, "bullet_points": {"credit_cost": 1, "display_name": "Bullet Points", "is_premium": false, "prompt": "Extract the key points from the following transcript and present them as a bulleted list."}, "charting_method": {"credit_cost": 3, "display_name": "Charting Method", "is_premium": true, "prompt": "If applicable, organize the information from the transcript into a chart or table format for easy comparison of facts."}, "clean_transcript": {"credit_cost": 1, "display_name": "Clean Transcript", "is_premium": false, "prompt": "Please clean up the following transcript by removing filler words (like \u0027um\u0027, \u0027uh\u0027, \u0027you know\u0027), correcting obvious grammar mistakes, and structuring it into clear paragraphs. Do not summarize, just clean the existing text."}, "concept_map": {"credit_cost": 3, "display_name": "Concept Map", "is_premium": true, "prompt": "Generate a concept map in a hierarchical list format, showing the main topics and their relationships from the transcript."}, "cornell_notes": {"credit_cost": 3, "display_name": "Cornell Notes", "is_premium": true, "prompt": "Format the key points of the transcript into the Cornell note-taking system, with sections for cues, notes, and a summary."}, "detailed_summary": {"credit_cost": 2, "display_name": "Detailed Summary", "is_premium": false, "prompt": "Provide a detailed, multi-paragraph summary of the following transcript, covering all main topics and arguments."}, "extract_ideas": {"credit_cost": 2, "display_name": "Extract Ideas", "is_premium": true, "prompt": "Brainstorm and extract creative ideas, concepts, and potential projects mentioned in the transcript."}, "extract_insights": {"credit_cost": 2, "display_name": "Extract Insights", "is_premium": true, "prompt": "Analyze the transcript to find deeper meanings, non-obvious connections, and key insights."}, "extract_main_idea": {"credit_cost": 2, "display_name": "Main Idea", "is_premium": true, "prompt": "Distill the entire transcript down to its single most important core message or main idea."}, "extract_patterns": {"credit_cost": 2, "display_name": "Extract Patterns", "is_premium": true, "prompt": "Identify and describe any recurring themes, arguments, or patterns in the transcript."}, "extract_predictions": {"credit_cost": 2, "display_name": "Extract Predictions", "is_premium": true, "prompt": "Identify and list any future predictions or forecasts made in the transcript."}, "extract_references": {"credit_cost": 2, "display_name": "Extract References", "is_premium": true, "prompt": "Extract any mentioned books, articles, tools, or other references from the transcript."}, "extract_tweet_ideas": {"credit_cost": 2, "display_name": "Extract Tweet Ideas", "is_premium": false, "prompt": "Extract several tweetable moments or quotes from the transcript."}, "extract_wisdom": {"credit_cost": 2, "display_name": "Extract Wisdom", "is_premium": true, "prompt": "Extract the most valuable wisdom, life lessons, and actionable advice from the transcript."}, "key_insights": {"credit_cost": 2, "display_name": "Key Insights", "is_premium": true, "prompt": "Analyze the following transcript and extract the most important insights and takeaways as a bulleted list."}, "linkedin_article_idea": {"credit_cost": 2, "display_name": "LinkedIn Article Idea", "is_premium": true, "prompt": "Create an outline for a professional LinkedIn article based on the transcript."}, "linkedin_post": {"credit_cost": 2, "display_name": "LinkedIn Post", "is_premium": true, "prompt": "Write a professional LinkedIn post summarizing the key takeaways from the transcript."}, "micro_summary": {"credit_cost": 1, "display_name": "Micro Summary", "is_premium": false, "prompt": "Summarize the following transcript in 1-2 concise sentences."}, "notable_quotes": {"credit_cost": 1, "display_name": "Notable Quotes", "is_premium": false, "prompt": "Identify and list several of the most notable or memorable quotes from the transcript."}, "outline_notes": {"credit_cost": 2, "display_name": "Outline Notes", "is_premium": false, "prompt": "Create a structured outline of the transcript, using headings and nested bullet points to organize the information."}, "proper_notes_generation": {"credit_cost": 4, "display_name": "Proper Notes", "is_premium": true, "prompt": "Generate a comprehensive and well-structured set of notes from the transcript, suitable for study or research."}, "qa_split_page": {"credit_cost": 2, "display_name": "Q\u0026A Split Page", "is_premium": true, "prompt": "Create a two-column layout with questions on the left and answers from the transcript on the right."}, "qec_method": {"credit_cost": 3, "display_name": "QEC Method", "is_premium": true, "prompt": "Structure the transcript\u0027s arguments using the Question-Evidence-Conclusion (QEC) method."}, "questions_answers": {"credit_cost": 2, "display_name": "Q\u0026A", "is_premium": true, "prompt": "Generate a list of questions and their corresponding answers based on the content of the transcript."}, "rapid_logging": {"credit_cost": 2, "display_name": "Rapid Logging", "is_premium": true, "prompt": "Convert the transcript into a rapid-logging format, using short, objective sentences and bullet points."}, "short_summary": {"credit_cost": 1, "display_name": "Short Summary", "is_premium": false, "prompt": "Provide a brief, single-paragraph summary of the following transcript."}, "speaker_identification": {"credit_cost": 4, "display_name": "Speaker ID", "is_premium": true, "prompt": "Analyze the transcript and attempt to label different speakers (e.g., Speaker 1, Speaker 2). This may not be perfectly accurate."}, "study_flashcards": {"credit_cost": 3, "display_name": "Flashcards", "is_premium": true, "prompt": "Create a list of flashcards (question/answer format) based on the key information in the transcript for studying."}, "tnote_method": {"credit_cost": 3, "display_name": "T-Note Method", "is_premium": true, "prompt": "Organize the transcript\u0027s information using the T-Note method, focusing on main concepts, definitions, and supporting details."}, "twitter_post_ideas": {"credit_cost": 2, "display_name": "Twitter Post Ideas", "is_premium": false, "prompt": "Generate 5 engaging tweet ideas based on the content of the transcript."}, "twitter_thread": {"credit_cost": 4, "display_name": "Twitter Thread (Full)", "is_premium": true, "prompt": "Generate a full Twitter thread with multiple tweets based on the transcript, complete with hooks and summaries."}, "twitter_thread_idea": {"credit_cost": 3, "display_name": "Twitter Thread Idea", "is_premium": true, "prompt": "Outline a compelling Twitter thread (3-5 tweets) based on a key topic from the transcript."}, "viral_clip_screener": {"credit_cost": 5, "display_name": "Viral Clip Screener", "is_premium": true, "prompt": "Imagine you are a social media manager. Identify 3-5 moments from this transcript that have the highest potential to become a viral short
1890-form video (like a TikTok or Reel). For each moment, describe the scene or quote and explain why it\u0027s compelling."}}; 1891 1892 // YouTube IFrame API Logic 1893 // YouTube IFrame API Logic 1894 let player; 1895 window.onYouTubeIframeAPIReady = function () { 1896 console.log("YouTube API Ready, initializing player..."); 1897 player = new YT.Player('youtube-player', { 1898 height: '100%', 1899 width: '100%', 1900 videoId: '', 1901 playerVars: { 1902 'playsinline': 1, 1903 'rel': 0, 1904 'modestbranding': 1, 1905 'enablejsapi': 1, 1906 'autoplay': 1 1907 }, 1908 events: { 1909 'onReady': onPlayerReady 1910 } 1911 }); 1912 }; 1913 1914 function onPlayerReady(event) { 1915 console.log("YouTube Player Ready event fired"); 1916 } 1917 1918 window.seekToTime = function (seconds) { 1919 const secs = parseFloat(seconds); 1920 console.log("Seeking to:", secs); 1921 if (player && typeof player.seekTo === 'function') { 1922 player.seekTo(secs, true); 1923 player.playVideo(); 1924 } else { 1925 console.warn("Player not fully ready, falling back to iframe src update"); 1926 const iframe = document.getElementById('youtube-player'); 1927 if (iframe) { 1928 const baseUrl = `https://www.youtube.com/embed/`; 1929 const params = `?start=${Math.floor(secs)}&autoplay=1&enablejsapi=1`; 1930 iframe.src = baseUrl + params; 1931 } else { 1932 console.error("Could not find youtube-player element for seeking fallback"); 1933 } 1934 } 1935 }; 1936 1937 // CLEAR CONTENT FUNCTION 1938 function clearAllGeneratedContent() { 1939 const container = document.querySelector('.generated-content-container'); 1940 const emptyState = document.querySelector('.generated-content-empty'); 1941 const clearBtn = document.getElementById('clear-all-content-btn'); 1942 1943 // Clear all child nodes (generated cards) 1944 while (container.firstChild) { 1945 container.removeChild(container.firstChild); 1946 } 1947 1948 // Append the empty/upsell state and ensure it's visible 1949 container.appendChild(emptyState); 1950 emptyState.style.display = 'flex'; // Use flex to match its inner layout 1951 1952 // Hide the clear button 1953 clearBtn.classList.add('hidden'); 1954 1955 // Adjust column heights after clearing 1956 if (window.adjustColumnHeights) { 1957 window.adjustColumnHeights(); 1958 } 1959 } 1960 1961 // GLOBAL STATE 1962 let IS_AUTHENTICATED = document.body.dataset.authenticated === 'true'; 1963 // Paying users only â a free registered account is not premium. 1964 const IS_PREMIUM = false; 1965 1966 const authModal = document.getElementById('auth_modal'); 1967 const transcriptCheckoutModal = document.getElementById('transcript_checkout_modal'); 1968 const confirmEmailModal = document.getElementById('confirm_email_modal'); 1969 const premiumRequiredModal = document.getElementById('premium_required_modal'); 1970 const upgradeFromConfirmBtn = document.getElementById('upgrade-from-confirm-btn'); 1971 const upgradeFromPremiumModalBtn = document.getElementById('upgrade-from-premium-modal-btn'); 1972 const resendConfirmBtn = document.getElementById('resend-confirm-btn'); 1973 const stripe = Stripe("pk_live_51GYV9XIHI17dZ9P7DQ88NMUhg7MBvq3VIhBNOxMw1v4a3HOPk0N92IdHXeHvsnQO48BAdyFA2fTtJewjf5pqyabu0071HXg26E"); 1974 let transcriptCheckout = null; 1975 1976 const toggleBtn = document.getElementById('toggle-timestamps-btn'); 1977 const copyBtn = document.getElementById('copy-btn'); 1978 const transcriptContent = document.getElementById('transcript-content'); 1979 const mostUsedContainer = document.querySelector('.most-used-list'); 1980 const mostUsedEmpty = document.querySelector('.most-used-empty'); 1981 1982 // ELEMENT REFERENCES 1983 const clearBtn = document.getElementById('clear-all-content-btn'); 1984 const scrollToAiBtn = document.getElementById('scroll-to-ai-btn'); 1985 const aiSection = document.getElementById('ai-section'); 1986 1987 // --- NEW: LOGIC FOR FREE TRIAL BUTTON IN MODAL --- 1988 const trialBtn = document.getElementById('trigger-trial-checkout'); 1989 1990 if (trialBtn) { 1991 trialBtn.addEventListener('click', async () => { 1992 // 1. Close the "Limit Reached" modal 1993 const trialModal = document.getElementById('free_trial_modal'); 1994 if (trialModal) trialModal.close(); 1995 1996 // 2. Open the Checkout Modal (Loading State) 1997 if (transcriptCheckoutModal) transcriptCheckoutModal.showModal(); 1998 1999 try { 2000 // 4. Call Backend 2001 const response = await fetch('/create-checkout-session', { 2002 method: 'POST', 2003 headers: { 'Content-Type': 'application/json' }, 2004 body: JSON.stringify({ 2005 planName: 'premium_yearly', 2006 returnUrl: window.location.href, 2007 isTrial: true 2008 }) 2009 }); 2010 2011 const { clientSecret, fallbackUrl, error } = await response.json(); 2012 2013 if (error || !clientSecret) { 2014 if (fallbackUrl) { window.location.href = fallbackUrl; return; } 2015 console.error('Server error:', error); 2016 document.getElementById('transcript-checkout').innerHTML = `<p class="text-red-500 text-center">Could not initialize payment. Please try again.</p>`; 2017 return; 2018 } 2019 2020 // 5. Mount Stripe â embedded with hosted fallback for ETP 2021 if (transcriptCheckout) { 2022 transcriptCheckout.destroy(); 2023 } 2024 try {
2025 transcriptCheckout = await stripe.initEmbeddedCheckout({ clientSecret }); 2026 document.getElementById('transcript-checkout').innerHTML = ''; 2027 transcriptCheckout.mount('#transcript-checkout'); 2028 } catch (innerE) { 2029 console.warn('Embedded init failed, falling back to hosted:', innerE); 2030 if (fallbackUrl) window.location.href = fallbackUrl; 2031 else throw innerE; 2032 } 2033 2034 } catch (e) { 2035 console.error("Fetch error:", e); 2036 document.getElementById('transcript-checkout').innerHTML = `<p class="text-red-500 text-center">Connection failed.</p>`; 2037 } 2038 }); 2039 } 2040 // --- END NEW LOGIC --- 2041 2042 // Define showUpgradeModal in a scope accessible by onclick attributes. 2043 // Behavior matches premium.html + audio_viewer.html (Batch B): 2044 // - reads BOTH clientSecret + fallbackUrl from server 2045 // - falls back to hosted Stripe URL if embedded init throws 2046 // (Firefox ETP / Brave / Safari ITP block m.stripe.network) 2047 window.showUpgradeModal = async function (isTrial = false) { 2048 // --- 1. GUEST CHECK: Redirect to Register if not logged in --- 2049 if (!IS_AUTHENTICATED) { 2050 window.location.href = "/register?intent=trial"; 2051 return; 2052 } 2053 // ------------------------------------------------------------- 2054 2055 if (!transcriptCheckoutModal) return; 2056 transcriptCheckoutModal.showModal(); 2057 2058 let payload; 2059 try { 2060 const response = await fetch('/create-checkout-session', { 2061 method: 'POST', 2062 headers: { 'Content-Type': 'application/json' }, 2063 body: JSON.stringify({ 2064 planName: 'premium_monthly', 2065 returnUrl: window.location.href, 2066 isTrial: isTrial 2067 }) 2068 }); 2069 payload = await response.json(); 2070 } catch (e) { 2071 document.getElementById('transcript-checkout').innerHTML = `<p class="text-red-500 text-center">Network error contacting our server.</p>`; 2072 return; 2073 } 2074 2075 const { clientSecret, fallbackUrl, error } = payload; 2076 2077 if (error || !clientSecret) { 2078 if (fallbackUrl) { window.location.href = fallbackUrl; return; } 2079 console.error('Server error:', error); 2080 document.getElementById('transcript-checkout').innerHTML = `<p class="text-red-500 text-center">Could not initialize payment. Please try again.</p>`; 2081 return; 2082 } 2083 2084 if (transcriptCheckout) { 2085 transcriptCheckout.destroy(); 2086 transcriptCheckout = null; 2087 } 2088 try { 2089 transcriptCheckout = await stripe.initEmbeddedCheckout({ clientSecret }); 2090 document.getElementById('transcript-checkout').innerHTML = ''; 2091 transcriptCheckout.mount('#transcript-checkout'); 2092 } catch (e) { 2093 // Embedded blocked (tracking-protection) â graceful fallback. 2094 console.warn('Embedded init failed, falling back to hosted:', e); 2095 if (fallbackUrl) { 2096 window.location.href = fallbackUrl; 2097 } else { 2098 document.getElementById('transcript-checkout').innerHTML = `<p class="text-red-500 text-center">Could not load checkout. ${e.message || ''}</p>`; 2099 } 2100 } 2101 } 2102 2103 function updateMostUsedFeatures() { 2104 if (!mostUsedContainer || !mostUsedEmpty || !IS_AUTHENTICATED) return; 2105 2106 const usageCount = JSON.parse(localStorage.getItem('featureUsageCount')) || {}; 2107 const sortedFeatures = Object.entries(usageCount) 2108 .sort(([, a], [, b]) => b - a) 2109 .slice(0, 5); 2110 2111 if (sortedFeatures.length === 0) { 2112 mostUsedContainer.classList.add('hidden'); 2113 mostUsedEmpty.classList.remove('hidden'); 2114 return; 2115 } 2116 2117 const buttonContainer = mostUsedContainer.querySelector('.flex-wrap'); 2118 buttonContainer.innerHTML = ''; // Clear existing buttons 2119 2120 for (const [featureType, count] of sortedFeatures) { 2121 const featureDetails = AI_FEATURES[featureType]; 2122 if (featureDetails) { 2123 const button = document.createElement('button'); 2124 button.className = 'btn btn-sm btn-ghost ai-preset-btn'; 2125 button.dataset.generationType = featureType; 2126 button.innerHTML = `<i class="ph-duotone ph-sparkle"></i> ${featureDetails.display_name}`; 2127 button.addEventListener('click', handleAiFeatureClick); 2128 buttonContainer.appendChild(button); 2129 } 2130 } 2131 2132 mostUsedContainer.classList.remove('hidden'); 2133 mostUsedEmpty.classList.add('hidden'); 2134 } 2135 2136 if (toggleBtn && transcriptContent) { 2137 toggleBtn.addEventListener('click', function () { 2138 const timestampsOn = this.dataset.timestampsOn === 'true'; 2139 const timestamps = transcriptContent.querySelectorAll('.transcript-timestamp'); 2140 2141 if (timestampsOn) { 2142 timestamps.forEach(ts => ts.style.display = 'none'); 2143 this.dataset.timestampsOn = 'false'; 2144 this.classList.add('btn-outline'); 2145 } else { 2146 timestamps.forEach(ts => ts.style.display = 'inline'); 2147 this.dataset.timestampsOn = 'true'; 2148 this.classList.remove('btn-outline'); 2149 } 2150 }); 2151 } 2152 2153 if (copyBtn && transcriptContent) { 2154 copyBtn.addEventListener('click', function () { 2155 const timestampsOn = toggleBtn ? toggleBtn.dataset.timestampsOn === 'true' : true; 2156 let textToCopy = ''; 2157 2158 transcriptContent.querySelectorAll('p').forEach(p => { 2159 let lineText = ''; 2160 if (timestampsOn) { 2161 const timestampEl = p.querySelector('.transcript-timestamp'); 2162 if (timestampEl && window.getComputedStyle(timestampEl).display !== 'none') { 2163 lineText += timestampEl.innerText.trim() + ' '; 2164 } 2165 } 2166 const textEl = p.querySelector('.transcript-text'); 2167 if (textEl) { 2168 lineText += textEl.innerText.trim(); 2169 } 2170 textToCopy += lineText + '\n'; 2171 }); 2172 2173 navigator.clipboard.writeText(textToCopy.trim()).then(() => { 2174 const originalText = copyBtn.innerHTML; 2175 copyBtn.innerHTML = '<i class="ph-duotone ph-check-circle text-[16px]"></i> Copied!'; 2176 setTimeout(() => { 2177 copyBtn.innerHTML = originalText; 2178 }, 2000); 2179 2180 // A successful copy is the best-timed offer on this page: they 2181 // came for the transcript, they have it, and they are about to 2182 // leave. 89% of visitors take exactly one transcript and never 2183 // return, so this is the only moment most of them are ever 2184 // reachable â the daily limit is 1, so they never hit a paywall. 2185 // 2186 // Delayed so the "Copied!" confirmation lands first; showing it 2187 // instantly would read as a punishment for copying. Once per 2188 // page view, and never for paying users. 2189 if (!IS_PREMIUM && !window.__copyOfferShown) { 2190 window.__copyOfferShown = true; 2191 setTimeout(function () { 2192 try { 2193 fetch('/api/track', { 2194 method: 'POST', credentials: 'same-origin', 2195 headers: {'Content-Type': 'application/json'}, 2196 body: JSON.stringify({event: 'copy_offer_shown', meta: { 2197 audience: IS_AUTHENTICATED ? 'member' : 'guest' 2198 }}) 2199 }); 2200 } catch (_) {} 2201 const m = document.getElementById('pay_prompt_modal'); 2202 // Guests see "Create Free Account" here, and it was a bare 2203 // /register: anyone who signed up from it lost this 2204 // transcript (register sends a no-next signup to /premium), 2205 // and the signup could not be told apart from any other. 2206 // Give it a return address and tag the source; SIGNUP rows 2207 // record the path, so copy signups are countable on their 2208 // own (details LIKE '%from=copy%'), separately from the 2209 // export gate's ?export=. Restored on close so a later 2210 // trigger of the same modal is not credited to copy. 2211 const signup = document.getElementById('pay-prompt-signup'); 2212 if (m && signup && !IS_AUTHENTICATED) { 2213 const plain = signup.getAttribute('href'); 2214 signup.setAttribute('href', plain + (plain.indexOf('?') === -1 ? '?' : '&') + 2215 'next=' + encodeURIComponent(window.location.pathname + '?from=copy')); 2216 m.addEventListener('close', function () { signup.setAttribute('href', plain); }, { once: true }); 2217 }
2218 if (m) { try { m.showModal(); } catch (_) { m.setAttribute('open', ''); } } 2219 }, 1800); 2220 } 2221 }).catch(err => { 2222 console.error('Failed to copy text: ', err); 2223 }); 2224 }); 2225 } 2226 2227 const leftColumn = document.querySelector('.w-full.md\\:w-1\\/2.lg\\:w-3\\/5'); 2228 const rightColumn = document.querySelector('.w-full.md\\:w-1\\/2.lg\\:w-2\\/5'); 2229 const goToTopBtn = document.getElementById('go-to-top-btn'); 2230 const pinBtn = document.getElementById('pin-btn'); 2231 const videoContainer = document.getElementById('video'); 2232 2233 if (pinBtn) { 2234 pinBtn.addEventListener('click', () => { 2235 videoContainer.classList.toggle('pinned'); 2236 }); 2237 } 2238 2239 window.adjustColumnHeights = function () { 2240 if (!leftColumn || !rightColumn) return; 2241 if (window.innerWidth >= 768) { 2242 leftColumn.style.height = 'auto'; 2243 rightColumn.style.height = 'auto'; 2244 const leftHeight = leftColumn.offsetHeight; 2245 const rightHeight = rightColumn.offsetHeight; 2246 if (rightHeight > leftHeight) { 2247 leftColumn.style.height = `${rightHeight}px`; 2248 } 2249 } else { 2250 leftColumn.style.height = 'auto'; 2251 rightColumn.style.height = 'auto'; 2252 } 2253 } 2254 2255 if (goToTopBtn) { 2256 goToTopBtn.addEventListener('click', function () { 2257 const videoElement = document.getElementById('video'); 2258 if (videoElement) { 2259 videoElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); 2260 } 2261 }); 2262 } 2263 2264 adjustColumnHeights(); 2265 const observer = new MutationObserver(adjustColumnHeights); 2266 if (rightColumn) { 2267 observer.observe(rightColumn, { childList: true, subtree: true }); 2268 } 2269 window.addEventListener('resize', adjustColumnHeights); 2270 2271 document.querySelectorAll('.category-header').forEach(header => { 2272 header.addEventListener('click', () => { 2273 const content = header.nextElementSibling; 2274 const icon = header.querySelector('i.ph-caret-down'); 2275 2276 content.classList.toggle('hidden'); 2277 icon.classList.toggle('rotate-180'); 2278 2279 setTimeout(adjustColumnHeights, 300); 2280 }); 2281 }); 2282 2283 document.querySelectorAll('.ai-preset-btn, .feature-btn').forEach(button => { 2284 button.addEventListener('click', handleAiFeatureClick); 2285 }); 2286 2287 async function handleAiFeatureClick(event) { 2288 if (!IS_AUTHENTICATED) { 2289 if (authModal) authModal.showModal(); 2290 return; 2291 } 2292 2293 const button = event.currentTarget; 2294 const generationType = button.dataset.generationType; 2295 if (!generationType) return; 2296 2297 const featureTitle = AI_FEATURES[generationType]?.display_name || generationType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); 2298 const languageSelector = document.querySelector('.ai-language-picker'); 2299 const selectedLanguage = languageSelector ? languageSelector.value : 'English'; 2300 2301 const usageCount = JSON.parse(localStorage.getItem('featureUsageCount')) || {}; 2302 usageCount[generationType] = (usageCount[generationType] || 0) + 1; 2303 localStorage.setItem('featureUsageCount', JSON.stringify(usageCount)); 2304 updateMostUsedFeatures(); 2305 2306 let fullTranscript = ''; 2307 document.querySelectorAll('.transcript-text').forEach(span => { 2308 fullTranscript += span.innerText.trim() + ' '; 2309 }); 2310 2311 if (fullTranscript.trim().length < 20) { 2312 alert("Transcript is too short to process."); 2313 return; 2314 } 2315 2316 const generatedContentContainer = document.querySelector('.generated-content-container'); 2317 const emptyState = document.querySelector('.generated-content-empty'); 2318 if (emptyState) emptyState.style.display = 'none'; 2319 if (clearBtn) clearBtn.classList.remove('hidden'); // Show clear button 2320 2321 const loadingCard = createContentCard('Generating...', '<div class="flex justify-center items-center p-4"><span class="loading loading-dots loading-lg"></span></div>', 'loading-card'); 2322 generatedContentContainer.prepend(loadingCard); 2323 adjustColumnHeights(); 2324 2325 try { 2326 const response = await fetch('/api/generate', { 2327 method: 'POST', 2328 headers: { 'Content-Type': 'application/json' }, 2329 body: JSON.stringify({ 2330 transcript: fullTranscript, 2331 feature: generationType, 2332 video_id: '', 2333 language: selectedLanguage 2334 }), 2335 }); 2336 2337 document.getElementById('loading-card')?.remove(); 2338 const result = await response.json(); 2339 2340 if (!response.ok) { 2341 if (response.status === 403 && result.errorCode === 'EMAIL_NOT_CONFIRMED') { 2342 if (confirmEmailModal) confirmEmailModal.showModal(); 2343 } else if (response.status === 403 && result.errorCode === 'PREMIUM_FEATURE_LOCKED') { 2344 const featureNameSpan = document.getElementById('premium-feature-name'); 2345 if (featureNameSpan) featureNameSpan.textContent = featureTitle; 2346 if (premiumRequiredModal) premiumRequiredModal.showModal(); 2347 2348 const upgradeLink = `<a href="javascript:void(0)" onclick="window.showUpgradeModal()" class="link link-primary">upgrade your plan</a>`; 2349 const errorMessage = `This is a premium feature. Please ${upgradeLink} to use it.`; 2350 const errorCard = createContentCard(`Error: ${featureTitle}`, errorMessage); 2351 generatedContentContainer.prepend(errorCard); 2352 2353 } else if (response.status === 429) { 2354 await window.showUpgradeModal(); 2355 } else { 2356 const errorCard = createContentCard('Error', `<p class="text-red-500">${result.error || 'An unknown error occurred.'}</p>`); 2357 generatedContentContainer.prepend(errorCard); 2358 } 2359 } else { 2360 const resultCard = createContentCard(featureTitle, result.content); 2361 generatedContentContainer.prepend(resultCard); 2362 } 2363 } catch (error) { 2364 document.getElementById('loading-card')?.remove(); 2365 const errorCard = createContentCard('Error', `<p class="text-red-500">Failed to connect to the server. Please try again.</p>`); 2366 generatedContentContainer.prepend(errorCard); 2367 console.error('API call failed:', error); 2368 } finally { 2369 adjustColumnHeights(); 2370 } 2371 } 2372 2373 if (transcriptCheckoutModal) { 2374 transcriptCheckoutModal.addEventListener('close', () => { 2375 if (transcriptCheckout) { 2376 transcriptCheckout.destroy(); 2377 transcriptCheckout = null; 2378 } 2379 document.getElementById('transcript-checkout').innerHTML = '<div class="flex justify-center items-center p-8"><span class="loading loading-dots loading-lg"></span></div>'; 2380 }); 2381 } 2382 2383 if (upgradeFromConfirmBtn && confirmEmailModal) { 2384 upgradeFromConfirmBtn.addEventListener('click', () => { 2385 confirmEmailModal.close(); 2386 window.showUpgradeModal(); 2387 }); 2388 } 2389
2390 if (upgradeFromPremiumModalBtn && premiumRequiredModal) { 2391 upgradeFromPremiumModalBtn.addEventListener('click', () => { 2392 premiumRequiredModal.close(); 2393 window.showUpgradeModal(); 2394 }); 2395 } 2396 2397 if (resendConfirmBtn) { 2398 resendConfirmBtn.addEventListener('click', async () => { 2399 const btnText = resendConfirmBtn.querySelector('.btn-text'); 2400 const btnLoader = resendConfirmBtn.querySelector('.btn-loader'); 2401 const messageDiv = document.getElementById('resend-message'); 2402 2403 resendConfirmBtn.disabled = true; 2404 btnText.textContent = 'Sending...'; 2405 btnLoader.classList.remove('hidden'); 2406 messageDiv.innerText = ''; 2407 2408 try { 2409 const response = await fetch('/resend-confirmation', { method: 'POST' }); 2410 const result = await response.json(); 2411 2412 if (response.ok) { 2413 messageDiv.innerText = result.message; 2414 messageDiv.classList.remove('text-red-600'); 2415 messageDiv.classList.add('text-green-600'); 2416 } else { 2417 messageDiv.innerText = result.error || 'An error occurred.'; 2418 messageDiv.classList.remove('text-green-600'); 2419 messageDiv.classList.add('text-red-600'); 2420 } 2421 } catch (e) { 2422 messageDiv.innerText = 'Failed to connect to server.'; 2423 messageDiv.classList.remove('text-green-600'); 2424 messageDiv.classList.add('text-red-600'); 2425 } finally { 2426 setTimeout(() => { 2427 resendConfirmBtn.disabled = false; 2428 btnText.textContent = 'Resend confirmation email'; 2429 btnLoader.classList.add('hidden'); 2430 }, 5000); 2431 } 2432 }); 2433 } 2434 2435 // SCROLL TO AI BUTTON LOGIC 2436 if (scrollToAiBtn && typeof aiColumnContainer !== 'undefined') { 2437 window.addEventListener('scroll', () => { 2438 const aiRect = aiColumnContainer.getBoundingClientRect(); 2439 if (aiRect.bottom < 0) { 2440 scrollToAiBtn.classList.remove('hidden'); 2441 } else { 2442 scrollToAiBtn.classList.add('hidden'); 2443 } 2444 }); 2445 2446 scrollToAiBtn.addEventListener('click', () => { 2447 aiColumnContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); 2448 }); 2449 } 2450 2451 function createContentCard(title, content, elementId = null) { 2452 const card = document.createElement('div'); 2453 card.className = 'bg-white/80 rounded-lg p-4 border border-blue-200 shadow-sm'; 2454 if (elementId) card.id = elementId; 2455 card.innerHTML = ` 2456 <div class="flex justify-between items-center mb-2"> 2457 <h4 class="font-bold text-gray-800">${title}</h4> 2458 <button class="btn btn-xs btn-ghost" onclick="copyCardContent(this)"> 2459 <i class="ph-duotone ph-copy"></i> Copy 2460 </button> 2461 </div> 2462 <div class="prose prose-sm max-w-none text-gray-700 content-area">${marked.parse(content)}</div> 2463 `; 2464 return card; 2465 } 2466 2467 updateMostUsedFeatures(); 2468 2469 // --- TRANSCRIPT SEARCH LOGIC --- 2470 const searchInput = document.getElementById('transcript-search'); 2471 if (searchInput) { 2472 searchInput.addEventListener('input', function (e) { 2473 const term = e.target.value.toLowerCase(); 2474 const lines = document.querySelectorAll('.transcript-line'); 2475 2476 lines.forEach(line => { 2477 const text = line.querySelector('.transcript-text').textContent.toLowerCase(); 2478 if (text.includes(term)) { 2479 line.classList.remove('hidden'); 2480 line.style.backgroundColor = term.length > 0 ? '#eff6ff' : 'transparent'; 2481 } else { 2482 line.classList.add('hidden'); 2483 } 2484 }); 2485 }); 2486 } 2487 2488 // Load YouTube API 2489 const tag = document.createElement('script'); 2490 tag.src = "https://www.youtube.com/iframe_api"; 2491 const firstScriptTag = document.getElementsByTagName('script')[0]; 2492 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 2493 2494 function copyCardContent(button) { 2495 const contentArea = button.closest('.bg-white\\/80').querySelector('.content-area'); 2496 navigator.clipboard.writeText(contentArea.innerText).then(() => { 2497 button.innerHTML = '<i class="ph-duotone ph-check-circle"></i> Copied!'; 2498 setTimeout(() => { 2499 button.innerHTML = '<i class="ph-duotone ph-copy"></i> Copy'; 2500 }, 2000); 2501 }); 2502 } 2503 2504 // transcript_viewer.html script section 2505 2506 function fetchDownloadFormats() { 2507 if (!IS_AUTHENTICATED) { 2508 const authModal = document.getElementById('auth_modal'); 2509 if (authModal && typeof authModal.showModal === 'function') { 2510 authModal.showModal(); 2511 } else { 2512 console.error("Auth modal not found or showModal not supported"); 2513 } 2514 return; 2515 } 2516 2517 2518 const modal = document.getElementById('download_modal'); 2519 const loader = document.getElementById('download_loader'); 2520 const optionsDiv = document.getElementById('download_options'); 2521 const headerText = document.getElementById('download_header_text'); 2522 2523 if (modal) modal.showModal(); 2524 if (loader) loader.classList.remove('hidden'); 2525 if (headerText) headerText.classList.remove('hidden'); 2526 if (optionsDiv) optionsDiv.innerHTML = ''; 2527 2528 const videoId = ""; 2529 const videoUrl = "https://www.youtube.com/watch?v=" + videoId; 2530 2531 fetch('/api/download/check-formats', { 2532 method: 'POST', 2533 headers: { 'Content-Type': 'application/json' }, 2534 body: JSON.stringify({ 2535 url: videoUrl, 2536 video_id: videoId 2537 }) 2538 }) 2539 .then(async response => { 2540 const data = await response.json(); 2541 if (loader) loader.classList.add('hidden'); 2542 2543 // 1. Handle Insufficient Credits (Upsell) 2544 if (response.status === 402) { 2545 // Close the current download modal first 2546 modal.close(); 2547 2548 // Trigger the Stripe checkout modal (Same as AI features) 2549 // Pass 'false' because downloads don't usually have a 'trial' version 2550 window.showUpgradeModal(false); 2551 return; 2552 } 2553 2554 // if (response.status === 402) { 2555 // headerText.classList.add('hidden'); 2556 // optionsDiv.innerHTML = ` 2557 // <div class="text-center py-6"> 2558 // <div class="bg-amber-50 border border-amber-200 p-6 rounded-xl"> 2559 // <i class="ph-duotone ph-lock-key text-5xl text-amber-500 mb-3"></i> 2560 // <h4 class="text-xl font-bold text-gray-800">Insufficient Credits</h4> 2561 // <p class="text-gray-600 mb-6">You need 2 credits to extract download links. You currently have ${data.current_credits} credits.</p> 2562 // <a href="/premium" class="btn btn-primary btn-wide"> 2563 // <i class="ph-duotone ph-rocket"></i> Upgrade or Buy Credits 2564 // </a> 2565 // </div> 2566 // </div> 2567 // `; 2568 // return; 2569 //} 2570 2571 if (!response.ok || data.status === 'error') { 2572 optionsDiv.innerHTML = `<div class="alert alert-error text-sm">Error: ${data.details || "Failed to fetch formats"}</div>`; 2573 return; 2574 } 2575 2576 // 2. Success: Render the Table with all formats 2577 let html = ` 2578 <div class="alert alert-success py-2 text-xs mb-4"> 2579 <i class="ph ph-check-circle"></i> 2 Credits deducted. Formats extracted successfully. 2580 </div> 2581 <table class="table table-xs w-full"> 2582 <thead> 2583 <tr class="bg-base-200"> 2584 <th>Type</th> 2585 <th>Resolution/Quality</th> 2586 <th>Format</th> 2587 <th>Size</th> 2588 <th class="text-right">Action</th> 2589 </tr> 2590 </thead> 2591 <tbody> 2592 `; 2593 2594 if (data.formats && data.formats.length > 0) {
2595 data.formats.forEach(f => { 2596 const badgeClass = f.type.includes('Video + Audio') ? 'badge-success' : (f.type.includes('Audio') ? 'badge-info' : 'badge-warning'); 2597 2598 html += ` 2599 <tr class="hover"> 2600 <td><span class="badge ${badgeClass} badge-outline badge-xs text-[10px] whitespace-nowrap">${f.type}</span></td> 2601 <td class="font-bold">${f.resolution}</td> 2602 <td><span class="badge badge-ghost badge-xs uppercase">${f.ext}</span></td> 2603 <td>${f.size}</td> 2604 <td class="text-right"> 2605 <a href="${f.direct_url}" 2606 target="_blank" 2607 rel="noopener noreferrer" 2608 class="btn btn-xs btn-warning text-black hover:bg-yellow-400 font-medium whitespace-nowrap"> 2609 <i class="ph-duotone ph-download-simple"></i> Download 2610 </a> 2611 </td> 2612 </tr> 2613 `; 2614 }); 2615 } else { 2616 html += `<tr><td colspan="5" class="text-center py-6 text-gray-500 italic">No formats found.</td></tr>`; 2617 } 2618 2619 html += '</tbody></table>'; 2620 optionsDiv.innerHTML = html; 2621 2622 // Update user's visual credit count if you have a credit display on page 2623 const creditDisplay = document.querySelector('.user-credits-count'); 2624 if (creditDisplay && data.new_balance !== undefined) { 2625 creditDisplay.innerText = data.new_balance; 2626 } 2627 }) 2628 .catch(err => { 2629 if (loader) loader.classList.add('hidden'); 2630 optionsDiv.innerHTML = `<div class="alert alert-error text-sm">Network Error: ${err.message}</div>`; 2631 }); 2632 } 2633 2634 // Helper function to handle UI after clicking download 2635 function confirmCharge(cost) { 2636 // Optional: Add a toast notification here if you have a toast library 2637 // Close modal after 1.5 seconds so user sees the download started 2638 setTimeout(() => { 2639 const modal = document.getElementById('download_modal'); 2640 if (modal) modal.close(); 2641 }, 1500); 2642 } 2643 2644 2645 2646</script>
2646 2647 2648 2649
2650<script> 2651 // Export gate: an account first, not a payment. 2652 // 2653 // On 11 Aug this was switched from "create a free account to export" to 2654 // "pay to export". New accounts fell from ~230/day to ~100/day and trials 2655 // from ~6/day to ~2.4/day, and neither recovered after the 16 Aug revert, 2656 // which left this gate as it was. The trial pipeline is members â all 71 2657 // trials since then were logged-in users, 61 of them via /premium â so a 2658 // gate that stops creating accounts starves it. Meanwhile the pay version 2659 // turned 527 member export clicks into zero trials. 2660 // 2661 // So: guests create an account, and the return address brings them back to 2662 // this transcript with the export they asked for already starting. Free 2663 // members get TXT/PDF/DOCX, as before 11 Aug. SRT/VTT are Pro, except that 2664 // every member gets one free â /api/export enforces both server side. 2665 const EXPORT_PRO_ONLY = ['srt', 'vtt']; 2666 const FREE_SUBTITLE_LEFT = false; 2667 2668 function _trackExportGate(format, action) { 2669 try { 2670 fetch('/api/track', { 2671 method: 'POST', credentials: 'same-origin', 2672 headers: {'Content-Type': 'application/json'}, 2673 body: JSON.stringify({event: 'export_gate_hit', meta: { 2674 format: format, 2675 audience: IS_AUTHENTICATED ? 'member' : 'guest', 2676 action: action 2677 }}) 2678 }); 2679 } catch (_) {} 2680 } 2681 2682 window.handleExportAuth = function (event, format) { 2683 if (IS_PREMIUM) return true; 2684 2685 if (IS_AUTHENTICATED) { 2686 if (EXPORT_PRO_ONLY.indexOf(format) === -1) return true; 2687 if (FREE_SUBTITLE_LEFT) return true; 2688 event.preventDefault(); 2689 _trackExportGate(format, 'paywall'); 2690 // Someone clicking SRT/VTT has said exactly what they want and why 2691 // ("I need this in my editor"), so the offer names it instead of 2692 // announcing a daily limit they have not hit. 2693 if (typeof window.openPaywall === 'function') { 2694 window.openPaywall({ 2695 title: 'Start your free trial to download ' + format.toUpperCase(), 2696 cta: 'export_' + format 2697 }); 2698 } else { 2699 const m = document.getElementById('pay_prompt_modal'); 2700 if (m) { try { m.showModal(); } catch (_) { m.setAttribute('open', ''); } } 2701 else { window.location.href = "/premium"; } 2702 } 2703 return false; 2704 } 2705 2706 event.preventDefault(); 2707 _trackExportGate(format, 'signup_prompt'); 2708 const next = window.location.pathname + '?export=' + encodeURIComponent(format); 2709 if (typeof window.openAuthModal === 'function') { 2710 window.openAuthModal({ 2711 title: 'Create a free account to download', 2712 text: 'Your ' + format.toUpperCase() + ' will start downloading as soon as youâre signed in. Itâs free, and this transcript will still be here.', 2713 next: next 2714 }); 2715 } else { 2716 window.location.href = "/register?next=" + encodeURIComponent(next); 2717 } 2718 return false; 2719 }; 2720 2721 // Coming back from signup with ?export=<format>: finish the download they 2722 // asked for, so creating the account is what delivered the file rather than 2723 // a detour they then have to repeat. The param is removed first so a refresh 2724 // or a back navigation cannot fire the download twice. 2725 (function () { 2726 const params = new URLSearchParams(window.location.search); 2727 const format = (params.get('export') || '').toLowerCase(); 2728 // `from` only exists to label where a signup came from; it has done its 2729 // job by the time they are back here, so keep it out of shared URLs. 2730 if (!format && !params.has('from')) return; 2731 params.delete('export'); 2732 params.delete('from'); 2733 const clean = window.location.pathname + (params.toString() ? '?' + params.toString() : '') + window.location.hash; 2734 try { history.replaceState(null, '', clean); } catch (_) {} 2735 2736 if (!format || !IS_AUTHENTICATED) return; 2737 // A new account arrives with its free subtitle download unused. 2738 if (EXPORT_PRO_ONLY.indexOf(format) !== -1 && !IS_PREMIUM && !FREE_SUBTITLE_LEFT) return; 2739 const link = document.querySelector('a[href^="/api/export/' + format + '/"]'); 2740 if (!link) return; 2741 setTimeout(function () { window.location.href = link.getAttribute('href'); }, 600); 2742 2743 // The download is a navigation to a file, so this page stays put. Offer 2744 // the trial once the file is on its way â after delivering what they 2745 // created the account for, not before, and never to someone already 2746 // paying. SRT/VTT are the natural next want for anyone exporting. 2747 if (IS_PREMIUM || window.__postExportOfferShown) return; 2748 window.__postExportOfferShown = true; 2749 setTimeout(function () { 2750 try { 2751 fetch('/api/track', { 2752 method: 'POST', credentials: 'same-origin', 2753 headers: {'Content-Type': 'application/json'}, 2754 body: JSON.stringify({event: 'post_export_offer_shown', meta: {format: format}}) 2755 }); 2756 } catch (_) {} 2757 if (typeof window.openPaywall === 'function') { 2758 window.openPaywall({ 2759 title: 'Your ' + format.toUpperCase() + ' is downloading', 2760 cta: 'post_export' 2761 }); 2762 } 2763 }, 4000); 2764 })(); 2765</script>
2765 2766 2767 2768 2769<!-- For rendering markdown from AI -->
2770<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
2770 2771 2772 2773 2774 2775 2776 2777 2778 </main> 2779 2780 <footer class="footer p-10 bg-neutral text-neutral-content"> 2781 <aside> 2782 <svg xmlns="http://www.w3.org/2000/svg" class="hidden w-12 h-12 fill-blue-500 sm:inline" 2783 viewBox="0 0 256 256"> 2784 <path d="M232,64V192a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H224A8,8,0,0,1,232,64Z" 2785 opacity="0.2"></path> 2786 <path 2787 d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM48,136a8,8,0,0,1,8-8H72a8,8,0,0,1,0,16H56A8,8,0,0,1,48,136Zm160,0a8,8,0,0,1-8,8H104a8,8,0,0,1,0-16h96A8,8,0,0,1,208,136Zm-48,32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h96A8,8,0,0,1,160,168Zm48,0a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16h16A8,8,0,0,1,208,168Z"> 2788 </path> 2789 </svg> 2790 <p>Transcript.you<br>© 2026 All rights reserved.</p> 2791 2792 <!-- Company & Legal Section (moved under copyright) --> 2793 <nav class="flex flex-col gap-2 mt-6"> 2794 <h6 class="footer-title">Company</h6> 2795 <a href="/refer" class="link link-hover">Refer & Earn</a> 2796 <a href="/affiliate" class="link link-hover">Affiliate Program</a> 2797 <a href="/blog/" class="link link-hover">Our Blog</a> 2798 <a href="/about" class="link link-hover">About Us</a> 2799 <a href="/contact" class="link link-hover">Contact</a> 2800 <h6 class="footer-title mt-4">Resources</h6> 2801 <a href="/transcription-guide" class="link link-hover">Transcription Guide</a> 2802 <a href="/glossary" class="link link-hover">Glossary</a> 2803 <a href="/vs" class="link link-hover">Tool Comparisons</a> 2804 <h6 class="footer-title mt-4">Explore by Topic</h6> 2805 <a href="/subtitles-and-captions" class="link link-hover">Subtitles & Captions</a> 2806 <a href="/audio-transcription" class="link link-hover">Audio Transcription</a> 2807 <a href="/video-transcription" class="link link-hover">Video Transcription</a> 2808 <a href="/output-formats" class="link link-hover">Output Formats</a> 2809 <a href="/use-cases" class="link link-hover">Use Cases</a> 2810 <h6 class="footer-title mt-4">For You</h6> 2811 <a href="/for/podcasters" class="link link-hover">For Podcasters</a> 2812 <a href="/for/students" class="link link-hover">For Students</a> 2813 <a href="/for/journalists" class="link link-hover">
2813For Journalists</a> 2814 <a href="/for/researchers" class="link link-hover">For Researchers</a> 2815 <a href="/for/content-creators" class="link link-hover">For Content Creators</a> 2816 <!-- Legal Section within same column --> 2817 <h6 class="footer-title mt-4">Legal</h6> 2818 <a href="/terms" class="link link-hover">Terms of Use</a> 2819 <a href="/privacy" class="link link-hover">Privacy Policy</a> 2820 </nav> 2821 </aside> 2822 <!-- START: MODIFIED SECTION --> 2823 2824 <!-- Audio Conversion Column --> 2825 <nav> 2826 <h6 class="footer-title">Audio Conversion</h6> 2827 <a class="link link-hover" href="/transcribe-audio">Transcribe Audio</a> 2828 <a class="link link-hover" href="/mp3-to-text">MP3 to Text</a> 2829 <a class="link link-hover" href="/wav-to-text">WAV to Text</a> 2830 <a class="link link-hover" href="/m4a-to-text">M4A to Text</a> 2831 <a class="link link-hover" href="/aac-to-text">AAC to Text</a> 2832 <a class="link link-hover" href="/ogg-to-text">OGG to Text</a> 2833 <a class="link link-hover" href="/flac-to-text">FLAC to Text</a> 2834 <a class="link link-hover" href="/wma-to-text">WMA to Text</a> 2835 <a class="link link-hover" href="/mp3-to-transcript">MP3 to 2836 Transcript</a> 2837 <a class="link link-hover" href="/speech-to-text">Speech to 2838 Text</a> 2839 <a class="link link-hover" href="/voice-to-text">Voice to 2840 Text</a> 2841 <a class="link link-hover" href="/audio-to-text">Audio to 2842 Text</a> 2843 <a class="link link-hover" href="/voice-memo-to-text">Voice Memo 2844 to Text</a> 2845 <a class="link link-hover" href="/dictation-to-text">Dictation 2846 to Text</a> 2847 </nav> 2848 2849 <!-- Video Conversion Column --> 2850 <nav> 2851 <h6 class="footer-title">Video Conversion</h6> 2852 <a class="link link-hover" href="/tube">Youtube Transcript</a> 2853 <a class="link link-hover" href="/mp4-to-text">MP4 to Text</a> 2854 <a class="link link-hover" href="/mov-to-text">MOV to Text</a> 2855 <a class="link link-hover" href="/mkv-to-text">MKV to Text</a> 2856 <a class="link link-hover" href="/avi-to-text">AVI to Text</a> 2857 <a class="link link-hover" href="/webm-to-text">WebM to Text</a> 2858 <a class="link link-hover" href="/wmv-to-text">WMV to Text</a> 2859 <a class="link link-hover" href="/transcribe-youtube-videos">Transcribe 2860 YouTube</a> 2861 <a class="link link-hover" href="/video-to-text">Video to 2862 Text</a> 2863 <a class="link link-hover" href="/transcribe-video">Transcribe 2864 Video</a> 2865 <a class="link link-hover" href="/video-to-transcript">Video to 2866 Transcript</a> 2867 <a class="link link-hover" href="/convert-video-to-text">Convert 2868 Video to Text</a> 2869 </nav> 2870 2871 <!-- Use Cases Column --> 2872 <nav> 2873 <h6 class="footer-title">Use Cases</h6> 2874 <a class="link link-hover" href="/zoom-to-text">Zoom to Text</a> 2875 <a class="link link-hover" href="/google-meet-to-text">Google 2876 Meet to Text</a> 2877 <a class="link link-hover" href="/teams-to-text">Teams to 2878 Text</a> 2879 <a class="link link-hover" href="/podcast-to-text">Podcast to 2880 Text</a> 2881 <a class="link link-hover" href="/interview-transcription">Interview 2882 Transcription</a> 2883 <a class="link link-hover" href="/meeting-transcription">Meeting 2884 Transcription</a> 2885 <a class="link link-hover" href="/lecture-to-text">Lecture to 2886 Text</a> 2887 <a class="link link-hover" href="/sermon-to-text">Sermon to 2888 Text</a> 2889 <a class="link link-hover" href="/legal-transcription">Legal 2890 Transcription</a> 2891 <a class="link link-hover" href="/medical-transcription">Medical 2892 Transcription</a> 2893 <a class="link link-hover" href="/convert-audio-to-microsoft-word">Audio to 2894 Word</a> 2895 </nav> 2896 2897 <!-- Tools & Downloaders Column --> 2898 <nav> 2899 <h6 class="footer-title">Media Downloaders</h6> 2900 <a class="link link-hover" href="/youtube-downloader">YouTube Downloader</a> 2901 <a class="link link-hover" href="/facebook-downloader">Facebook Downloader</a> 2902 <a class="link link-hover" href="/tiktok-downloader">TikTok Downloader</a> 2903 <a class="link link-hover" href="/tiktok">TikTok Transcript</a> 2904 <a class="link link-hover" href="/bulk-tiktok">Bulk TikTok Transcripts</a> 2905 <a class="link link-hover" href="/instagram-downloader">Instagram Downloader</a> 2906 <a class="link link-hover" href="/x-downloader">Twitter (X) Downloader</a> 2907 </nav> 2908 2909 <!-- Subtitles & Captions Column --> 2910 <nav> 2911 <h6 class="footer-title">Subtitles & Captions</h6> 2912 <a class="link link-hover font-semibold" href="/subtitles-and-captions">All Subtitle Tools</a> 2913 <a class="link link-hover" href="/subtitle-downloader">Download YouTube Subtitles</a> 2914 <a class="link link-hover" href="/generate-subtitles">Generate Subtitles</a> 2915 <a class="link link-hover" href="/audio-to-srt">Audio to SRT</a> 2916 <a class="link link-hover" href="/video-to-srt">Video to SRT</a> 2917 <a class="link link-hover" href="/audio-to-vtt">Audio to VTT</a> 2918 <a class="link link-hover" href="/video-to-vtt">Video to VTT</a> 2919 <a class="link link-hover" href="/add-captions-to-video">Add Captions to Video</a> 2920 <a class="link link-hover" href="/free-subtitles">Free Subtitles</a> 2921</nav> 2922 2923 <nav> 2924 <h6 class="footer-title">Featured Articles</h6> 2925 2926 <a href="/blog/best-voice-to-text-tools" class="link link-hover">Best Voice-to-Text Tools 2026</a> 2927 2928 <a href="/blog/best-tiktok-transcript-tool" class="link link-hover">Best TikTok Transcript Tool 2026</a> 2929 2930 <a href="/blog/best-speech-to-text-software-2026" class="link link-hover">
2930Best Speech-to-Text Software 2026</a> 2931 2932 <a href="/blog/best-audio-to-text-converter" class="link link-hover">Best Audio-to-Text Converter 2026</a> 2933 2934 <a href="/blog/best-transcription-software-online" class="link link-hover">Best Transcription Software Online</a> 2935 2936 </nav> 2937 2938 </footer> 2939 <div class="footer footer-center p-4 bg-neutral text-neutral-content border-t border-white/5"> 2940 <aside> 2941 <p class="text-xs opacity-40">Transcript.You is not affiliated with YouTube, TikTok, or any other 2942 third-party platform mentioned on this site. Our services are for educational and personal use 2943 only.</p> 2944 </aside> 2945 </div> 2946 </div> 2947 <div class="drawer-side z-[60]"> 2948 <label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label> 2949 <div class="flex flex-col h-full w-4/5 bg-base-100 text-base-content overflow-y-auto"> 2950 <div class="p-4 pb-40"> 2951 <!-- Sidebar Header --> 2952 <div class="flex justify-between items-center mb-4"> 2953 <a href="/" class="btn btn-ghost normal-case text-xl"> 2954 <svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 fill-blue-500" viewBox="0 0 256 256"> 2955 <path 2956 d="M232,64V192a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H224A8,8,0,0,1,232,64Z" 2957 opacity="0.2"></path> 2958 <path 2959 d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM48,136a8,8,0,0,1,8-8H72a8,8,0,0,1,0,16H56A8,8,0,0,1,48,136Zm160,0a8,8,0,0,1-8,8H104a8,8,0,0,1,0-16h96A8,8,0,0,1,208,136Zm-48,32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h96A8,8,0,0,1,160,168Zm48,0a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16h16A8,8,0,0,1,208,168Z"> 2960 </path> 2961 </svg> 2962 Transcript.you 2963 </a> 2964 <label for="my-drawer" class="btn btn-ghost btn-circle">â</label> 2965 </div> 2966 <!-- Sidebar Menu --> 2967 <!-- START: MODIFIED SECTION --> 2968 <ul class="menu p-0"> 2969 <!-- PRIMARY FUNCTIONS FIRST --> 2970 <li><a href="/tube" class="py-3"><i 2971 class="ph-duotone ph-youtube-logo text-red-600"></i> YouTube Transcript</a></li> 2972 <li><a href="/shorts" class="py-3"><i 2973 class="ph-duotone ph-video-camera text-orange-600"></i> Shorts</a></li> 2974 <li><a href="/tiktok" class="py-3"><i 2975 class="ph-duotone ph-tiktok-logo text-slate-800"></i> TikTok</a></li> 2976 <li><a href="/transcribe-audio" class="py-3"><i 2977 class="ph-duotone ph-microphone-stage text-purple-600"></i> Audio</a> 2978 </li> 2979 <li class="group"> 2980 <details class="[&_summary::-webkit-details-marker]:hidden"> 2981 <summary 2982 class="flex items-center justify-between py-3 cursor-pointer hover:bg-base-200 transition-colors list-none text-gray-700 rounded-lg"> 2983 <div class="flex items-center"> 2984 <i class="ph-duotone ph-subtitles text-violet-600 mr-3"></i> 2985 <a href="/subtitles-and-captions" 2986 onclick="event.stopPropagation();" 2987 class="hover:underline">Subtitles</a> 2988 </div> 2989 <i class="ph-bold ph-caret-down text-[10px] transition-transform group-open:rotate-180 mr-4 opacity-50"></i> 2990 </summary> 2991 <ul class="menu menu-sm p-0 pl-4 border-l border-base-200 ml-5 mt-1"> 2992 <li><a href="/subtitle-downloader" class="py-2">Download YouTube Subtitles</a></li> 2993 <li><a href="/generate-subtitles" class="py-2">Generate Subtitles</a></li> 2994 <li><a href="/video-to-srt" class="py-2">Video to SRT</a></li> 2995 <li><a href="/audio-to-srt" class="py-2">Audio to SRT</a></li> 2996 <li><a href="/add-captions-to-video" class="py-2">Add Captions to Video</a></li> 2997 <li><a href="/subtitles-and-captions" class="py-2 font-semibold">All Subtitle & Caption Tools</a></li> 2998 </ul> 2999 </details> 3000 </li> 3001 <li class="group"> 3002 <details class="[&_summary::-webkit-details-marker]:hidden"> 3003 <summary 3004 class="flex items-center justify-between py-3 cursor-pointer hover:bg-base-200 transition-colors list-none text-gray-700 rounded-lg"> 3005 <div class="flex items-center"> 3006 <i class="ph-duotone ph-stack text-indigo-600 mr-3"></i> 3007 3008 <a href="/bulk" 3009 onclick="event.stopPropagation();" 3010 class="hover:underline">Bulk Transcriptions</a> 3011 </div> 3012 <i class="ph-bold ph-caret-down text-[10px] transition-transform group-open:rotate-180 mr-4 opacity-50"></i> 3013 </summary> 3014 <ul class="menu menu-sm p-0 pl-4 border-l border-base-200 ml-5 mt-1"> 3015 <li><a href="/bulk-transcripts" class="py-2"><i 3016 class="ph-duotone ph-youtube-logo text-red-500"></i> Bulk YouTube Transcripts</a></li> 3017 <li><a href="/bulk-tiktok" class="py-2"><i 3018 class="ph-duotone ph-tiktok-logo text-pink-500"></i> Bulk TikTok Transcripts</a></li> 3019 <li><a href="/bulk-spotify" class="py-2"><i 3020 class="ph-duotone ph-spotify-logo text-green-500"></i> Bulk Spotify Transcripts</a></li> 3021 <li><a href="/bulk-vimeo" class="py-2"><i 3022 class="ph-duotone ph-video text-sky-500"></i> Bulk Vimeo Transcripts</a></li> 3023 <li><a href="/bulk-twitch" class="py-2"><i 3024 class="ph-duotone ph-twitch-logo text-purple-600"></i> Bulk Twitch Transcripts</a></li> 3025 <li><a href="/bulk-audio" class="py-2"><i 3026 class="ph-duotone ph-microphone text-emerald-500"></i> Bulk Audio Transcription</a></li> 3027 </ul> 3028 </details> 3029 </li> 3030 3031 <li><a href="/files" class="py-3"><i 3032 class="ph-duotone ph-folder-open text-blue-600"></i> Files <span 3033 class="task-count-badge ml-1"></span></a></li> 3034 <li class="group"> 3035 <details class="[&_summary::-webkit-details-marker]:hidden"> 3036 <summary 3037 class="flex items-center justify-between py-3 cursor-pointer hover:bg-base-200 transition-colors list-none text-gray-700 rounded-lg"> 3038 <div class="flex items-center"> 3039 <i class="ph-duotone ph-download-simple text-indigo-600 mr-3"></i>
3040 <span>Media Downloaders</span> 3041 </div> 3042 <i 3043 class="ph-bold ph-caret-down text-[10px] transition-transform group-open:rotate-180 mr-4 opacity-50"></i> 3044 </summary> 3045 <ul class="menu menu-sm p-0 pl-4 border-l border-base-200 ml-5 mt-1"> 3046 <li><a href="/youtube-downloader" class="py-2"><i 3047 class="ph-duotone ph-youtube-logo text-red-600"></i> YouTube 3048 Downloader</a> 3049 </li> 3050 <li><a href="/facebook-downloader" class="py-2"><i 3051 class="ph-duotone ph-facebook-logo text-blue-600"></i> Facebook 3052 Downloader</a></li> 3053 <li><a href="/tiktok-downloader" class="py-2"><i 3054 class="ph-duotone ph-tiktok-logo text-slate-800"></i> TikTok 3055 Downloader</a> 3056 </li> 3057 <li><a href="/instagram-downloader" class="py-2"><i 3058 class="ph-duotone ph-instagram-logo text-pink-600"></i> Instagram 3059 Downloader</a></li> 3060 <li><a href="/x-downloader" class="py-2"><i 3061 class="ph-duotone ph-x-logo text-slate-900"></i> Twitter (X) 3062 Downloader</a> 3063 </li> 3064 <li><a href="/subtitle-downloader" class="py-2"><i 3065 class="ph-duotone ph-subtitles text-violet-600"></i> Subtitle 3066 Downloader</a> 3067 </li> 3068 </ul> 3069 </details> 3070 </li> 3071 3072 <li><a href="/refer" class="py-3 text-indigo-600 font-bold"><i 3073 class="ph-duotone ph-gift text-indigo-600"></i> 3074 Refer</a></li> 3075 3076 <div class="divider my-2 opacity-50">Resources</div> 3077 <li><a href="/transcription-guide" class="py-3"><i 3078 class="ph-duotone ph-book-open text-blue-600"></i> Transcription Guide</a></li> 3079 <li><a href="/glossary" class="py-3"><i 3080 class="ph-duotone ph-list-magnifying-glass text-emerald-600"></i> Glossary</a></li> 3081 <li><a href="/vs" class="py-3"><i 3082 class="ph-duotone ph-scales text-amber-600"></i> Tool Comparisons</a></li> 3083 3084 <div class="divider my-2 opacity-50">For You</div> 3085 <li><a href="/for/podcasters" class="py-3"><i class="ph-duotone ph-microphone text-purple-600"></i> Podcasters</a></li> 3086 <li><a href="/for/students" class="py-3"><i class="ph-duotone ph-graduation-cap text-blue-600"></i> Students</a></li> 3087 <li><a href="/for/journalists" class="py-3"><i class="ph-duotone ph-newspaper text-slate-700"></i> Journalists</a></li> 3088 <li><a href="/for/researchers" class="py-3"><i class="ph-duotone ph-flask text-emerald-600"></i> Researchers</a></li> 3089 <li><a href="/for/content-creators" class="py-3"><i class="ph-duotone ph-video text-rose-600"></i> Content Creators</a></li> 3090 3091 3092 </ul> 3093 <!-- END: MODIFIED SECTION --> 3094 </div> 3095 <!-- Auth buttons at the bottom --> 3096 <div class="mt-auto p-4 border-t border-base-200 bg-base-100 sticky bottom-0 z-10"> 3097 3098 <div class="space-y-2"> 3099 <a href="/premium" 3100 class="btn btn-ghost w-full border border-gray-300">View Pricing</a> 3101 <a href="/register" class="btn btn-warning w-full"> 3102 <i class="ph-duotone ph-user-plus text-lg mr-2"></i> 3103 Register 3104 </a> 3105 <a href="/login" class="btn btn-primary w-full"> 3106 <i class="ph-duotone ph-sign-in text-lg mr-2"></i> 3107 Login 3108 </a> 3109 </div> 3110 3111 </div> 3112 </div> 3113 </div> 3114 </div> 3115 3116
3116<script> 3117 // Bookmark shortcut OS detection 3118 document.addEventListener('DOMContentLoaded', function () { 3119 if (navigator.userAgent.indexOf('Mac OS X') != -1) { 3120 document.querySelectorAll('.macos').forEach(el => el.classList.remove('hidden')); 3121 } else { 3122 document.querySelectorAll('.window').forEach(el => el.classList.remove('hidden')); 3123 } 3124 }); 3125 </script>
3125 3126 3127
3127<script> 3128 document.addEventListener('DOMContentLoaded', function () { 3129 const isAuthenticated = document.body.dataset.authenticated === 'true'; 3130 3131 // Only run this for guest users 3132 if (!isAuthenticated) { 3133 try { 3134 const guestTasks = JSON.parse(localStorage.getItem('guestTranscriptionTasks')) || []; 3135 const count = guestTasks.length; 3136 3137 if (count > 0) { 3138 // Find all badge elements and update them 3139 document.querySelectorAll('.task-count-badge').forEach(el => { 3140 el.textContent = `(${count})`; 3141 }); 3142 } 3143 } catch (e) { 3144 console.error("Could not parse guest tasks from localStorage:", e); 3145 } 3146 } 3147 }); 3148 </script>
3148 3149 3150
3150<script> 3151 // Function to set a cookie 3152 function setCookie(name, value, days) { 3153 var expires = ""; 3154 if (days) { 3155 var date = new Date(); 3156 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 3157 expires = "; expires=" + date.toUTCString(); 3158 } 3159 document.cookie = name + "=" + (value || "") + expires + "; path=/"; 3160 } 3161 3162 // Function to get URL parameter 3163 function getParam(p) { 3164 var match = RegExp('[?&]' + p + '=([^&]*)').exec(window.location.search); 3165 return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); 3166 } 3167 3168 // 1. Check if gclid is in the URL 3169 var gclid = getParam('gclid'); 3170 3171 // 2. If found, save it to a cookie for 90 days 3172 if (gclid) { 3173 setCookie('gclid', gclid, 90); 3174 } 3175 </script>
3175 3176
3177<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"c4d9333929ba4b06b0a7f41cb7c20dd2","r":1,"spa":2}' crossorigin="anonymous"></script>
3177 3178</body> 3179 3180</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.