nixamp 0.27.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +14 -4
  2. package/backtoschool/dist/assets/{index-6vtCMzi1.css → index-OE2xWZUH.css} +1 -1
  3. package/backtoschool/dist/assets/index-pQ_uH4bZ.js +93 -0
  4. package/backtoschool/dist/index.html +25 -9
  5. package/dist/accounts.d.ts +2 -0
  6. package/dist/accounts.js +9 -2
  7. package/dist/classroom.d.ts +15 -0
  8. package/dist/classroom.js +54 -0
  9. package/dist/live-events.d.ts +7 -1
  10. package/dist/live-events.js +88 -12
  11. package/dist/live-voice.d.ts +1 -1
  12. package/dist/live-voice.js +7 -7
  13. package/dist/password-reset-page.d.ts +2 -0
  14. package/dist/password-reset-page.js +106 -0
  15. package/dist/password-reset.d.ts +35 -0
  16. package/dist/password-reset.js +119 -0
  17. package/dist/server.d.ts +3 -0
  18. package/dist/server.js +96 -2
  19. package/dist/shared-translation.js +25 -17
  20. package/dist/tokens.d.ts +2 -0
  21. package/dist/tokens.js +2 -0
  22. package/dist/translation-passes.d.ts +13 -8
  23. package/dist/translation-passes.js +31 -20
  24. package/dist/upgrade-allowance.d.ts +23 -0
  25. package/dist/upgrade-allowance.js +70 -0
  26. package/package.json +1 -1
  27. package/src/accounts.ts +8 -2
  28. package/src/classroom.ts +52 -0
  29. package/src/live-events.ts +81 -13
  30. package/src/live-voice.ts +7 -7
  31. package/src/password-reset-page.ts +108 -0
  32. package/src/password-reset.ts +134 -0
  33. package/src/server.ts +77 -2
  34. package/src/shared-translation.ts +20 -14
  35. package/src/tokens.ts +3 -0
  36. package/src/translation-passes.ts +35 -25
  37. package/src/types/auth-system.d.ts +10 -0
  38. package/src/upgrade-allowance.ts +69 -0
  39. package/web/dist/assets/{hls-3VKVEQE3-P3sj2d3a.js → hls-3VKVEQE3-CAL4qBEq.js} +1 -1
  40. package/web/dist/assets/index-CLeEWlms.js +3 -0
  41. package/web/dist/assets/{index-BmR7T0zX.css → index-D34dTY18.css} +1 -1
  42. package/web/dist/assets/{mpegts-CZ1YoF1U.js → mpegts-H9oC2fAK.js} +1 -1
  43. package/web/dist/assets/{mpegts-LO6RVLD6-DuNM0Zxm.js → mpegts-LO6RVLD6-Bkd9Sqr6.js} +1 -1
  44. package/web/dist/index.html +6 -5
  45. package/web/dist/sw.js +6 -6
  46. package/backtoschool/dist/assets/index-D7Oas1m5.js +0 -88
  47. package/web/dist/assets/index-Do9VYiU8.js +0 -3
@@ -1 +1 @@
1
- import{t as e}from"./index-Do9VYiU8.js";var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule||!o.call(e,`default`)?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=[[/^(hvc1|hev1)/i,`H.265`],[/^av01/i,`AV1`],[/^(vp09|vp9)/i,`VP9`],[/^(ec-3|ec3)/i,`Dolby Digital Plus audio`],[/^(ac-3|ac3)/i,`Dolby Digital audio`],[/^dts/i,`DTS audio`],[/^(mp4a\.69|mp4a\.6b)/i,`MP2 audio`],[/^mp3$/i,`MP3 audio`],[/^mp4a\.40\./i,`AAC audio`]];function d(e){if(!e)return null;for(let[t,n]of u)if(t.test(e))return n;return e}function f(e,t){if(!t||!e)return[];if(e===`audio`){if(/^mp4a\.40\./i.test(t))return[`audio/mp4; codecs="mp4a.40.2"`];if(/^mp3$/i.test(t))return[`audio/mpeg`,`audio/mp4; codecs="mp3"`];if(/^opus$/i.test(t))return[`audio/mp4; codecs="opus"`,`audio/mp4; codecs="Opus"`]}return[`${e}/mp4; codecs="${t}"`]}function p(e,t,n=``,r=`stream`){if(!e||typeof t!=`function`)return null;let i=[[`video`,e.videoCodec],[`audio`,e.audioCodec]],a=[];for(let[e,n]of i)if(n&&!f(e,n).some(e=>{try{return t(e)}catch{return!1}})){let e=d(n);e&&a.push(e)}if(a.length===0)return null;let o=a.length===1?a[0]:`${String(a[0])} and ${String(a[1])}`;return`This ${r} is ${String(o)}, which this browser cannot decode.${n?` ${n}`:``}`}var m=2e3,h=5e3,g=3;function _(e,t={}){return{enableWorker:t.enableWorker??!1,enableStashBuffer:!0,stashInitialSize:393216,liveBufferLatencyChasing:!1,liveBufferLatencyMaxLatency:5,liveBufferLatencyMinRemain:1,autoCleanupSourceBuffer:!0,autoCleanupMaxBackwardDuration:30,autoCleanupMinBackwardDuration:10,lazyLoad:!1,lazyLoadMaxDuration:60,lazyLoadRecoverDuration:30,seekType:`range`}}async function v(t,n={}){let{media:r,src:i,isTv:a}=t,o=await e(()=>import(`./mpegts-CZ1YoF1U.js`).then(e=>l(e.default,1)),[]),s=o.default??o;if(!s.getFeatureList().mseLivePlayback)return t.onError(`This browser cannot play transport streams.`),{destroy:()=>void 0,levels:()=>[]};let c=_(a,n),u=null,d=!1,f=0,v=null,y=null,b=-1,x=0,S=()=>{v&&clearTimeout(v),y&&clearInterval(y),v=null,y=null},C=()=>{if(!u)return;let e=u;u=null;try{e.destroy()}catch{}},w=e=>{d||(d=!0,S(),C(),t.onError(e))},T=e=>{if(!d){if(f>=5){w(e);return}f+=1,S(),C(),t.onNotice(`Reconnecting\u2026 (${String(f)}/5)`),v=setTimeout(()=>{v=null,d||D()},m*2**(f-1))}},E=()=>{y&&clearInterval(y),b=r.currentTime,x=0,y=setInterval(()=>{if(!d&&u){if(r.paused||r.ended||r.seeking){x=0,b=r.currentTime;return}if(r.currentTime===b){x+=1,x>=g&&(x=0,T(`The stream stopped sending.`));return}b=r.currentTime,x=0}},h)};function D(){u=s.createPlayer({type:`mpegts`,isLive:t.live,url:i,withCredentials:n.withCredentials??!1},c),u.on(s.Events.MEDIA_INFO??`media_info`,(...e)=>{let t=e[0],r=p(t,e=>globalThis.MediaSource?.isTypeSupported?.(e)??!1,n.unplayableAdvice??``);r&&w(r)}),u.on(s.Events.ERROR??`error`,(...e)=>{let t=String(e[1]??``);T(`This stream could not be played (${t}).`)}),u.attachMediaElement(r),u.load(),E(),t.onReady?.({live:t.live,levels:[]})}let O=()=>{f=0,t.onNotice(null)};return r.addEventListener(`playing`,O),D(),{destroy(){d=!0,S(),r.removeEventListener(`playing`,O),C()},levels:()=>[]}}export{v as createMpegtsEngine,s as t};
1
+ import{t as e}from"./index-CLeEWlms.js";var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule||!o.call(e,`default`)?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=[[/^(hvc1|hev1)/i,`H.265`],[/^av01/i,`AV1`],[/^(vp09|vp9)/i,`VP9`],[/^(ec-3|ec3)/i,`Dolby Digital Plus audio`],[/^(ac-3|ac3)/i,`Dolby Digital audio`],[/^dts/i,`DTS audio`],[/^(mp4a\.69|mp4a\.6b)/i,`MP2 audio`],[/^mp3$/i,`MP3 audio`],[/^mp4a\.40\./i,`AAC audio`]];function d(e){if(!e)return null;for(let[t,n]of u)if(t.test(e))return n;return e}function f(e,t){if(!t||!e)return[];if(e===`audio`){if(/^mp4a\.40\./i.test(t))return[`audio/mp4; codecs="mp4a.40.2"`];if(/^mp3$/i.test(t))return[`audio/mpeg`,`audio/mp4; codecs="mp3"`];if(/^opus$/i.test(t))return[`audio/mp4; codecs="opus"`,`audio/mp4; codecs="Opus"`]}return[`${e}/mp4; codecs="${t}"`]}function p(e,t,n=``,r=`stream`){if(!e||typeof t!=`function`)return null;let i=[[`video`,e.videoCodec],[`audio`,e.audioCodec]],a=[];for(let[e,n]of i)if(n&&!f(e,n).some(e=>{try{return t(e)}catch{return!1}})){let e=d(n);e&&a.push(e)}if(a.length===0)return null;let o=a.length===1?a[0]:`${String(a[0])} and ${String(a[1])}`;return`This ${r} is ${String(o)}, which this browser cannot decode.${n?` ${n}`:``}`}var m=2e3,h=5e3,g=3;function _(e,t={}){return{enableWorker:t.enableWorker??!1,enableStashBuffer:!0,stashInitialSize:393216,liveBufferLatencyChasing:!1,liveBufferLatencyMaxLatency:5,liveBufferLatencyMinRemain:1,autoCleanupSourceBuffer:!0,autoCleanupMaxBackwardDuration:30,autoCleanupMinBackwardDuration:10,lazyLoad:!1,lazyLoadMaxDuration:60,lazyLoadRecoverDuration:30,seekType:`range`}}async function v(t,n={}){let{media:r,src:i,isTv:a}=t,o=await e(()=>import(`./mpegts-H9oC2fAK.js`).then(e=>l(e.default,1)),[]),s=o.default??o;if(!s.getFeatureList().mseLivePlayback)return t.onError(`This browser cannot play transport streams.`),{destroy:()=>void 0,levels:()=>[]};let c=_(a,n),u=null,d=!1,f=0,v=null,y=null,b=-1,x=0,S=()=>{v&&clearTimeout(v),y&&clearInterval(y),v=null,y=null},C=()=>{if(!u)return;let e=u;u=null;try{e.destroy()}catch{}},w=e=>{d||(d=!0,S(),C(),t.onError(e))},T=e=>{if(!d){if(f>=5){w(e);return}f+=1,S(),C(),t.onNotice(`Reconnecting\u2026 (${String(f)}/5)`),v=setTimeout(()=>{v=null,d||D()},m*2**(f-1))}},E=()=>{y&&clearInterval(y),b=r.currentTime,x=0,y=setInterval(()=>{if(!d&&u){if(r.paused||r.ended||r.seeking){x=0,b=r.currentTime;return}if(r.currentTime===b){x+=1,x>=g&&(x=0,T(`The stream stopped sending.`));return}b=r.currentTime,x=0}},h)};function D(){u=s.createPlayer({type:`mpegts`,isLive:t.live,url:i,withCredentials:n.withCredentials??!1},c),u.on(s.Events.MEDIA_INFO??`media_info`,(...e)=>{let t=e[0],r=p(t,e=>globalThis.MediaSource?.isTypeSupported?.(e)??!1,n.unplayableAdvice??``);r&&w(r)}),u.on(s.Events.ERROR??`error`,(...e)=>{let t=String(e[1]??``);T(`This stream could not be played (${t}).`)}),u.attachMediaElement(r),u.load(),E(),t.onReady?.({live:t.live,levels:[]})}let O=()=>{f=0,t.onNotice(null)};return r.addEventListener(`playing`,O),D(),{destroy(){d=!0,S(),r.removeEventListener(`playing`,O),C()},levels:()=>[]}}export{v as createMpegtsEngine,s as t};
@@ -17,8 +17,8 @@
17
17
  <meta property="og:title" content="nixamp: broadcast live radio, TV and film from your own machine" />
18
18
  <meta property="og:description" content="Launch your own live radio, TV and film network from your machine. Listen free, go live in one command, get paid with x402." />
19
19
  <meta property="og:type" content="website" />
20
- <script type="module" crossorigin src="/assets/index-Do9VYiU8.js"></script>
21
- <link rel="stylesheet" crossorigin href="/assets/index-BmR7T0zX.css">
20
+ <script type="module" crossorigin src="/assets/index-CLeEWlms.js"></script>
21
+ <link rel="stylesheet" crossorigin href="/assets/index-D34dTY18.css">
22
22
  </head>
23
23
  <body>
24
24
  <nav class="skip-links" aria-label="Skip to a section">
@@ -83,7 +83,7 @@
83
83
  </div>
84
84
  </section>
85
85
 
86
- <section class="panel player-only" data-title="Now Playing" aria-label="Now Playing">
86
+ <section class="panel player-only classroom-player" data-title="Now Playing" aria-label="Now Playing">
87
87
  <h2 class="panel-heading">Now Playing</h2>
88
88
  <span class="panel-tools" id="translation-buy-tools"></span>
89
89
  <video id="video" playsinline hidden></video>
@@ -504,14 +504,15 @@
504
504
  <div class="purchase-heading"><h2 id="translation-purchase-title">Buy translated audio</h2><button type="button" id="translation-purchase-close" aria-label="Close translated audio purchase">✕</button></div>
505
505
  <p id="translation-purchase-summary">Prepaid translation for live channels, video and audio.</p>
506
506
  <p id="translation-purchase-balance"></p>
507
+ <p class="hint" id="translation-free-description"></p>
507
508
  <label for="translation-plan">Pass</label><select id="translation-plan"></select>
508
509
  <p class="hint" id="translation-plan-description"></p>
509
510
  <label for="translation-coin">Pay with</label><select id="translation-coin"></select>
510
511
  <p class="hint">Crypto checkout through CoinPay. Any network fees are shown at checkout.</p>
511
- <a href="#account-panel" id="translation-sign-in">Sign in to purchase</a>
512
+ <a href="#account-panel" id="translation-sign-in">Sign in for free sessions or to purchase</a>
512
513
  <div class="purchase-actions"><button type="button" class="button" id="translation-checkout" disabled>Buy pass</button><a id="translation-checkout-link" target="_blank" rel="noopener noreferrer" hidden>Open secure checkout ↗ (new tab)</a><button type="button" id="translation-check-payment" hidden>Check payment</button></div>
513
514
  <details class="purchase-terms"><summary>Usage rates and expiry</summary>
514
- <p class="hint">Usage costs 5× the base speech API rates: $0.25 per 1,000 voice characters plus $1.10 per submitted audio hour. Live recognition includes repeated context, usually three submitted hours per listening hour. Shared live audio has the same access price for every listener.</p>
515
+ <p class="hint">After the five free sessions, new sessions draw from purchased credit at 5× the base speech API rates: $0.25 per 1,000 voice characters plus $1.10 per submitted audio hour. Live recognition includes repeated context, usually three submitted hours per listening hour. Shared live audio has the same paid access price for every listener.</p>
515
516
  <p class="hint">Unused credit expires. Buying before expiry adds credit and keeps the later expiry date. Your pass works across players on this account.</p>
516
517
  </details>
517
518
  <p id="translation-purchase-note" role="status" aria-live="polite" aria-atomic="true"></p>
package/web/dist/sw.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1789326867399";
2
+ const CACHE = "nixamp-1789330339689";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/.well-known/openaccess.json",
6
6
  "/apple-touch-icon.png",
7
7
  "/assets/background.worker-DH8ag5Q4.js",
8
- "/assets/hls-3VKVEQE3-P3sj2d3a.js",
8
+ "/assets/hls-3VKVEQE3-CAL4qBEq.js",
9
9
  "/assets/hls-n74Cnh8A.js",
10
- "/assets/index-BmR7T0zX.css",
11
- "/assets/index-Do9VYiU8.js",
12
- "/assets/mpegts-CZ1YoF1U.js",
13
- "/assets/mpegts-LO6RVLD6-DuNM0Zxm.js",
10
+ "/assets/index-CLeEWlms.js",
11
+ "/assets/index-D34dTY18.css",
12
+ "/assets/mpegts-H9oC2fAK.js",
13
+ "/assets/mpegts-LO6RVLD6-Bkd9Sqr6.js",
14
14
  "/assets/native-C7JTKWJH-BUyIoj0P.js",
15
15
  "/favicon.png",
16
16
  "/hero.png",
@@ -1,88 +0,0 @@
1
- (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=class extends Error{status;constructor(e,t){super(e),this.status=t}};async function t(t,n={}){let r=await fetch(t,{credentials:`same-origin`,...n,headers:{...n.body?{"content-type":`application/json`}:{},...n.headers}}),i={};try{i=await r.json()}catch{i={}}if(!r.ok)throw new e(String((i&&typeof i==`object`?i:{}).error??`That did not work.`),r.status);return i}function n(e,n,r=`POST`){return t(e,{method:r,body:JSON.stringify(n)})}var r=document.querySelector(`#main`),i=document.querySelector(`#account-button`),a=document.querySelector(`#account-dialog`),o=document.querySelector(`#account-form`),s=document.querySelector(`#account-title`),c=document.querySelector(`#account-copy`),l=document.querySelector(`#account-error`),u=document.querySelector(`#account-mode`),d=document.querySelector(`#event-dialog`),f=document.querySelector(`#event-form`),p=document.querySelector(`#event-form-title`),m=document.querySelector(`#event-kicker`),h=document.querySelector(`#event-submit`),g=document.querySelector(`#event-error`),_=document.querySelector(`#schedule-fields`),v=null,y=!1,b=!1,x=null,S=null,C=null,w=null;function T(e){return String(e??``).replaceAll(`&`,`&amp;`).replaceAll(`<`,`&lt;`).replaceAll(`>`,`&gt;`).replaceAll(`"`,`&quot;`).replaceAll(`'`,`&#039;`)}function E(e){return`/live/${encodeURIComponent(e.slug)}`}function D(){let e=new URLSearchParams(location.search).get(`invite`);return e?`?invite=${encodeURIComponent(e)}`:``}function O(e,t={}){return e?new Intl.DateTimeFormat(void 0,{weekday:`short`,month:`short`,day:`numeric`,hour:`numeric`,minute:`2-digit`,...t}).format(new Date(e)):`Time to be announced`}function k(e){return e.status===`live`?`Live now`:e.status===`ended`?e.recordingId?`Replay available`:`Event ended`:e.status===`cancelled`?`Cancelled`:O(e.startsAt)}function A(e){return C?.permissions.includes(e)??!1}function j(){i.textContent=v?v.email.split(`@`)[0]||`Account`:`Sign in`,i.classList.toggle(`signed-in`,!!v)}async function M(){try{v=(await t(`/api/v1/auth/me`)).account}catch{v=null}j()}function N(e){if(v){a.showModal(),s.textContent=`Signed in as ${v.email}`,c.textContent=`Your BackToSchool identity is your NixAmp account.`,o.hidden=!0;return}x=e??null,o.hidden=!1,l.textContent=``,P(),a.showModal()}function P(){s.textContent=y?`Create your account`:`Welcome back`,c.textContent=y?`One NixAmp account works here and everywhere NixAmp goes.`:`Sign in to host, chat, or raise your hand.`,u.textContent=y?`Already have an account? Sign in`:`New here? Create an account`;let e=o.elements.namedItem(`password`);e.autocomplete=y?`new-password`:`current-password`,o.querySelector(`button[type="submit"]`).textContent=y?`Create account`:`Sign in`}function F(e){if(!v){N(()=>F(e));return}b=e===`scheduled`,_.hidden=!b,p.textContent=b?`Schedule a live`:`Go live`,m.textContent=b?`Put it on the calendar`:`Start a conversation`,h.textContent=b?`Schedule live`:`Create live`,g.textContent=``;let t=f.elements.namedItem(`startsAt`);if(t.required=b,b&&!t.value){let e=new Date(Date.now()+36e5);e.setMinutes(Math.ceil(e.getMinutes()/15)*15,0,0),t.value=`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,`0`)}-${String(e.getDate()).padStart(2,`0`)}T${String(e.getHours()).padStart(2,`0`)}:${String(e.getMinutes()).padStart(2,`0`)}`}d.showModal()}function I(e){history.pushState(null,``,e),Z()}function L(e){let t=e.status===`live`||e.status===`starting`;return`
2
- <article class="event-card ${t?`is-live`:``}">
3
- <a href="${E(e)}" data-link>
4
- <div class="event-card-top">
5
- <span class="status-pill ${t?`status-live`:``}">${t?`Live`:T(k(e))}</span>
6
- ${e.topic?`<span class="topic">${T(e.topic)}</span>`:``}
7
- </div>
8
- <h3>${T(e.title)}</h3>
9
- <p>${T(e.description||`Drop in and learn something with us.`)}</p>
10
- <span class="event-link">${t?`Listen live`:`See event`}<span aria-hidden="true">→</span></span>
11
- </a>
12
- </article>`}async function R(){document.title=`BackToSchool.help — Learn something live`;let{events:e}=await t(`/api/v1/events?limit=60`).catch(()=>({events:[]})),n=e.filter(e=>e.status===`live`||e.status===`starting`),i=e.filter(e=>e.status===`scheduled`||e.status===`draft`),a=e.filter(e=>e.status===`ended`&&e.recordingId);r.innerHTML=`
13
- <section class="hero">
14
- <div class="hero-copy">
15
- <span class="eyebrow">Go back to school whenever you want</span>
16
- <h1>Learn something<br /><em>live.</em></h1>
17
- <p>Drop into thoughtful conversations, ask real questions, or start a room about something you know.</p>
18
- <div class="hero-actions">
19
- <button class="button button-primary" type="button" data-event-mode="live"><span class="live-dot"></span> Go live</button>
20
- <button class="button button-secondary" type="button" data-event-mode="scheduled">Schedule live</button>
21
- </div>
22
- <p class="hero-note">Listening is always one tap away. No account required.</p>
23
- </div>
24
- <div class="hero-board" aria-label="What is happening today">
25
- <span class="tape tape-one" aria-hidden="true"></span>
26
- <span class="tape tape-two" aria-hidden="true"></span>
27
- <div class="board-rule"></div>
28
- <span class="board-label">Today’s lesson</span>
29
- <strong>${T(n[0]?.title??i[0]?.title??`The best teachers are curious people`)}</strong>
30
- <p>${n[0]?`Happening right now`:i[0]?k(i[0]):`Host the first live conversation`}</p>
31
- ${n[0]?`<a href="${E(n[0])}" data-link class="board-action">Listen now <span>↗</span></a>`:`<button type="button" data-event-mode="live" class="board-action">Start a room <span>↗</span></button>`}
32
- <div class="board-doodles" aria-hidden="true"><span>?</span><span>✦</span><span>≈</span></div>
33
- </div>
34
- </section>
35
-
36
- <section class="event-section" id="live">
37
- <div class="section-heading">
38
- <div><span class="eyebrow">Walk in anytime</span><h2>Live now</h2></div>
39
- <span class="section-count">${n.length} ${n.length===1?`room`:`rooms`}</span>
40
- </div>
41
- <div class="event-grid">
42
- ${n.length?n.map(L).join(``):`<div class="empty-card"><span>☕</span><h3>The halls are quiet.</h3><p>Start a live and give people something worth dropping into.</p><button class="text-button" data-event-mode="live">Open a room →</button></div>`}
43
- </div>
44
- </section>
45
-
46
- <section class="event-section event-section-tint" id="upcoming">
47
- <div class="section-heading">
48
- <div><span class="eyebrow">Save your seat</span><h2>Coming up</h2></div>
49
- </div>
50
- <div class="upcoming-list">
51
- ${i.length?i.map(e=>`
52
- <a class="upcoming-row" href="${E(e)}" data-link>
53
- <time>${T(O(e.startsAt,{weekday:`short`,month:`short`,day:`numeric`}))}</time>
54
- <span><strong>${T(e.title)}</strong><small>${T(e.topic||`Live conversation`)}</small></span>
55
- <span class="row-arrow">→</span>
56
- </a>`).join(``):`<p class="empty-line">Nothing scheduled yet. Your idea could be next.</p>`}
57
- </div>
58
- </section>
59
-
60
- ${a.length?`<section class="event-section"><div class="section-heading"><div><span class="eyebrow">Listen after class</span><h2>Recent replays</h2></div></div><div class="event-grid">${a.map(L).join(``)}</div></section>`:``}
61
-
62
- <section class="host-callout">
63
- <span class="eyebrow">Know a thing or two?</span>
64
- <h2>Someone wants to hear it.</h2>
65
- <p>You don’t need a studio or a syllabus. Bring an idea and start talking.</p>
66
- <button class="button button-light" type="button" data-event-mode="scheduled">Plan a conversation</button>
67
- </section>`}function z(e){let t=D(),n=e.recordingId&&e.status===`ended`?`/api/v1/recordings/${encodeURIComponent(e.recordingId)}`:`/api/channels/${encodeURIComponent(e.roomId)}${t}`,r=e.status===`live`||!!e.recordingId;return`
68
- <div class="listen-card ${r?``:`waiting`}">
69
- <div class="sound-orbit" aria-hidden="true"><span></span><span></span><span></span><b>▶</b></div>
70
- <div>
71
- <span class="eyebrow">${e.status===`live`?`On air now`:e.recordingId?`Replay`:`The room opens soon`}</span>
72
- <h2>${r?e.status===`live`?`Listen live`:`Listen again`:`We’ll see you here.`}</h2>
73
- <p id="player-note">${r?`Press play and you’re in. No sign-up, no setup.`:T(k(e))}</p>
74
- ${r?`<button class="button button-primary listen-button" type="button" data-listen><span class="live-dot"></span> ${e.status===`live`?`Listen live`:`Play replay`}</button><audio id="event-audio" preload="none" src="${T(n)}" controls></audio>`:``}
75
- </div>
76
- </div>`}function B(e){return e.chatEnabled?`
77
- <div id="chat-messages" class="chat-messages"><p class="muted">Loading the conversation…</p></div>
78
- ${v?`<form id="chat-form" class="inline-form"><input name="body" maxlength="1000" placeholder="Add to the conversation" aria-label="Chat message" required /><button class="button button-small" type="submit">Send</button></form>`:`<button class="text-button" type="button" data-sign-in>Sign in to join the chat →</button>`}`:`<div class="panel-empty">Chat is off for this event.</div>`}function V(e,t){switch(e.type){case`event-header`:return`<div class="event-heading"><span class="status-pill ${t.status===`live`?`status-live`:``}">${T(k(t))}</span>${t.topic?`<span class="topic">${T(t.topic)}</span>`:``}<h1>${T(t.title)}</h1><p>${T(t.description||`A live conversation powered by NixAmp.`)}</p></div>`;case`player`:return z(t);case`stage`:return`<div class="stage-card"><span class="stage-avatar">${T(t.title.slice(0,1).toUpperCase())}</span><div><span class="eyebrow">Host stage</span><h2>${T(t.title)}</h2><p id="broadcast-note">${t.status===`live`?`This event is live.`:`Start when you’re ready.`}</p></div></div>`;case`host`:return`<div class="host-line"><span class="avatar">N</span><div><small>Hosted with</small><strong>NixAmp Live</strong></div></div>`;case`about`:return`<p class="reading-copy">${T(t.description||`Come listen, learn, and ask a question live.`)}</p>${t.topic?`<span class="topic topic-large">${T(t.topic)}</span>`:``}`;case`join`:return v?`<p class="panel-empty">You’re signed in and ready to participate.</p>`:`<div class="join-line"><div><strong>Want to ask something?</strong><p>Join with your NixAmp account.</p></div><button class="button button-secondary" type="button" data-sign-in>Join in</button></div>`;case`chat`:return B(t);case`questions`:return`<div class="panel-empty">Questions shared in chat can be brought onto the stage.</div>`;case`resources`:return`<div class="panel-empty">The host hasn’t added resources yet.</div>`;case`participants`:return`<div class="metric"><strong id="listener-count">—</strong><span>listening now</span></div>`;case`speakers`:return`<div class="people-row"><span class="avatar">H</span><span><strong>Host</strong><small>On stage</small></span></div>`;case`raise-hand`:return t.handRaiseEnabled?`<div class="join-line"><div><strong>Have something to add?</strong><p>Let the host know you’d like to speak.</p></div><button class="button button-secondary" type="button" data-raise-hand>Raise hand</button></div>`:null;case`hand-raises`:return`<div id="hand-raises" class="hand-raises"><p class="muted">No hands raised.</p></div>`;case`invite`:return`<form id="invite-form" class="form-stack compact"><label>Email<input name="email" type="email" placeholder="someone@example.com" required /></label><label>Invite as<select name="role"><option value="listener">Listener</option><option value="speaker">Speaker</option><option value="moderator">Moderator</option></select></label><button class="button button-small" type="submit">Create invite</button><p id="invite-note" class="form-note"></p></form>`;case`share`:return`<div class="share-box"><input id="share-url" value="${T(location.href)}" readonly aria-label="Event URL" /><button class="button button-small" type="button" data-copy>Copy link</button></div>`;case`event-controls`:return`<div class="control-stack"><button class="button button-primary" type="button" data-broadcast>${t.status===`live`?`Use this microphone`:`Start live audio`}</button>${t.status===`live`?`<button class="button button-danger" type="button" data-end-event>End event</button>`:``}</div>`;case`schedule`:return`<div class="metric"><strong>${T(O(t.startsAt))}</strong><span>${T(t.timezone)}</span></div>`;case`recording`:return`<div class="control-line"><span>${t.recordingEnabled?`Recording requested`:`Recording is off`}</span><span class="status-dot ${t.recordingEnabled?`on`:``}"></span></div>`;default:return null}}function H(e,t){try{if(!e.enabled||!e.visible||(e.permissions??[]).some(e=>!A(e)))return``;let n=V(e,t);if(n===null)return``;let r=e.title||e.type.replaceAll(`-`,` `);return`<section class="event-panel panel-${T(e.type)}" data-panel="${T(e.id)}"><span class="panel-title">${T(r)}</span>${n}</section>`}catch{return`<section class="event-panel panel-failed"><span class="panel-title">Panel unavailable</span><p>This part didn’t load. The audio player is unaffected.</p></section>`}}async function U(e){let n=await t(`/api/v1/events/${encodeURIComponent(e)}${D()}`);C=n;let i=n.event;document.title=`${i.title} — BackToSchool.help`,document.querySelector(`meta[name="description"]`)?.setAttribute(`content`,i.description||`Listen to ${i.title} live on BackToSchool.help.`),document.querySelector(`meta[property="og:title"]`)?.setAttribute(`content`,i.title),document.querySelector(`meta[property="og:description"]`)?.setAttribute(`content`,i.description||`Learn something live.`);let a=new Map;for(let e of[...n.layout.panels].sort((e,t)=>e.order-t.order)){let t=H(e,i);if(!t)continue;let n=a.get(e.region)??[];n.push(t),a.set(e.region,n)}r.innerHTML=`
79
- <div class="event-shell">
80
- <a class="back-link" href="/" data-link>← Explore more lives</a>
81
- <div class="event-layout">
82
- <div class="region region-primary">${(a.get(`primary`)??[]).join(``)}</div>
83
- <aside class="region region-secondary">${(a.get(`secondary`)??[]).join(``)}</aside>
84
- ${(a.get(`sidebar`)??[]).length?`<aside class="region region-sidebar">${a.get(`sidebar`).join(``)}</aside>`:``}
85
- ${(a.get(`bottom`)??[]).length?`<div class="region region-bottom">${a.get(`bottom`).join(``)}</div>`:``}
86
- ${(a.get(`drawer`)??[]).length?`<div class="region region-drawer">${a.get(`drawer`).join(``)}</div>`:``}
87
- </div>
88
- </div>`,W(i)}function W(r){document.querySelector(`[data-listen]`)?.addEventListener(`click`,async e=>{let t=e.currentTarget,n=document.querySelector(`#event-audio`);if(n){t.disabled=!0,t.textContent=`Connecting…`;try{await n.play(),t.textContent=`You’re listening`,document.querySelector(`#player-note`).textContent=`Live audio from this NixAmp room.`}catch{t.disabled=!1,t.textContent=`Try listening again`,document.querySelector(`#player-note`).textContent=`The host may still be getting ready. Try again in a moment.`}}});let i=document.querySelector(`#chat-form`);i?.addEventListener(`submit`,e=>{e.preventDefault();let t=i.elements.namedItem(`body`),a=t.value.trim();a&&(t.value=``,n(`/api/v1/events/${encodeURIComponent(r.id)}/chat`,{body:a}).then(()=>G(r)).catch(e=>{t.placeholder=e instanceof Error?e.message:`Could not send`}))});let a=document.querySelector(`#invite-form`);if(a?.addEventListener(`submit`,e=>{e.preventDefault();let n=new FormData(a),i=document.querySelector(`#invite-note`);i.textContent=`Creating invite…`,t(`/api/v1/events/${encodeURIComponent(r.id)}/invitations`,{method:`POST`,body:JSON.stringify({email:n.get(`email`),role:n.get(`role`)})}).then(async({inviteUrl:e,sent:t})=>{await navigator.clipboard?.writeText(e).catch(()=>void 0),i.textContent=t?`Invite sent. The link is copied too.`:`Invite link copied: ${e}`,a.reset()}).catch(e=>{i.textContent=e instanceof Error?e.message:`Could not invite`})}),document.querySelector(`[data-raise-hand]`)?.addEventListener(`click`,t=>{let i=t.currentTarget;i.disabled=!0,n(`/api/v1/events/${encodeURIComponent(r.id)}/hand-raises`,{}).then(()=>{i.textContent=`Hand raised ✓`}).catch(t=>{i.disabled=!1,i.textContent=t instanceof e&&t.status===401?`Sign in to raise hand`:`Try again`})}),document.querySelector(`[data-broadcast]`)?.addEventListener(`click`,()=>void J()),document.querySelector(`[data-end-event]`)?.addEventListener(`click`,()=>void X()),document.querySelector(`[data-copy]`)?.addEventListener(`click`,async e=>{await navigator.clipboard.writeText(location.href),e.currentTarget.textContent=`Copied ✓`}),r.chatEnabled){G(r);let e=window.setInterval(()=>void G(r),4e3),t=A(`event.moderate`)?window.setInterval(()=>void K(r),3e3):0;K(r),S=()=>{clearInterval(e),t&&clearInterval(t)}}q(r)}async function G(e){let n=document.querySelector(`#chat-messages`);if(n)try{let{messages:r}=await t(`/api/v1/events/${encodeURIComponent(e.id)}/chat${D()}`);n.innerHTML=r.length?r.map(e=>`<div class="chat-message"><span class="avatar avatar-small">${T(e.authorName.slice(0,1).toUpperCase())}</span><p><strong>${T(e.authorName)}</strong><span>${T(e.body)}</span></p></div>`).join(``):`<p class="muted">No messages yet. Say hello when you’re ready.</p>`,n.scrollTop=n.scrollHeight}catch{n.innerHTML=`<p class="muted">Chat is taking a break. The audio still works.</p>`}}async function K(e){let n=document.querySelector(`#hand-raises`);if(n&&A(`event.moderate`))try{let{handRaises:r}=await t(`/api/v1/events/${encodeURIComponent(e.id)}/hand-raises`);n.innerHTML=r.length?r.map(e=>`<div class="raise-row"><span><strong>${T(e.displayName)}</strong><small>${T(e.state)}</small></span><div><button class="text-button" data-hand="${T(e.accountId)}" data-state="invited">Invite</button><button class="text-button" data-hand="${T(e.accountId)}" data-state="dismissed">Dismiss</button></div></div>`).join(``):`<p class="muted">No hands raised.</p>`}catch{n.innerHTML=`<p class="muted">Could not refresh hand raises.</p>`}}async function q(e){let n=document.querySelector(`#listener-count`);if(n)try{let{channels:r}=await t(`/api/channels`);n.textContent=String(r.find(t=>t.id===e.roomId)?.listeners??0)}catch{n.textContent=`0`}}async function J(){if(!C||w)return;let e=document.querySelector(`[data-broadcast]`),n=document.querySelector(`#broadcast-note`);try{e?.setAttribute(`disabled`,`true`),n&&(n.textContent=`Asking for your microphone…`);let r=C.event;if(r.status!==`live`){let e=await t(`/api/v1/events/${encodeURIComponent(r.id)}/start`,{method:`POST`,body:JSON.stringify({version:r.version})});C=e,r=e.event}let i=await navigator.mediaDevices.getUserMedia({audio:!0}),a=[`audio/webm;codecs=opus`,`audio/webm`,`audio/ogg;codecs=opus`].find(e=>MediaRecorder.isTypeSupported(e))??``,o=new MediaRecorder(i,a?{mimeType:a}:void 0),s={recorder:o,stream:i,queue:Promise.resolve()};w=s,o.addEventListener(`dataavailable`,e=>{e.data.size&&(s.queue=s.queue.then(async()=>{if(!(await fetch(`/api/channels/${encodeURIComponent(r.roomId)}/chunk?format=${encodeURIComponent(o.mimeType||`webm`)}&name=${encodeURIComponent(r.title)}`,{method:`POST`,credentials:`same-origin`,headers:{"content-type":o.mimeType||`audio/webm`},body:e.data})).ok)throw Error(`NixAmp could not publish this audio`)}))}),o.start(1e3),e&&(e.disabled=!1,e.textContent=`Stop this microphone`,e.dataset.stopBroadcast=`true`,e.onclick=()=>void Y()),n&&(n.textContent=`Your microphone is live through NixAmp.`)}catch(t){e&&(e.disabled=!1),n&&(n.textContent=t instanceof Error?t.message:`Could not start the microphone.`)}}async function Y(){let e=w;if(!e)return;w=null;let t=new Promise(t=>e.recorder.addEventListener(`stop`,()=>t(),{once:!0}));e.recorder.stop(),e.stream.getTracks().forEach(e=>e.stop()),await t,await e.queue.catch(()=>void 0);let n=document.querySelector(`#broadcast-note`);n&&(n.textContent=`This microphone is off. The event is still open.`);let r=document.querySelector(`[data-broadcast]`);r&&(r.textContent=`Use this microphone`,r.onclick=()=>void J())}async function X(){if(!C)return;await Y();let e=C.event;try{C=await t(`/api/v1/events/${encodeURIComponent(e.id)}/end`,{method:`POST`,body:JSON.stringify({version:e.version})}),await fetch(`/api/channels/${encodeURIComponent(e.roomId)}`,{method:`DELETE`,credentials:`same-origin`}),await U(e.slug)}catch(e){let t=document.querySelector(`#broadcast-note`);t&&(t.textContent=e instanceof Error?e.message:`Could not end the event.`)}}async function Z(){S?.(),S=null,C=null;let t=/^\/live\/([^/]+)\/?$/.exec(location.pathname);try{t?await U(decodeURIComponent(t[1])):await R()}catch(t){r.innerHTML=`<section class="error-page"><span class="eyebrow">Class dismissed?</span><h1>${T(t instanceof e&&t.status===404?`That live event isn’t available.`:`We couldn’t open this page.`)}</h1><p>${T(t instanceof Error?t.message:`Try again in a moment.`)}</p><a href="/" data-link class="button button-primary">Back to live events</a></section>`}r.focus({preventScroll:!0})}i.addEventListener(`click`,()=>{if(!v){N();return}confirm(`Signed in as ${v.email}. Sign out?`)&&t(`/api/v1/auth/logout`,{method:`POST`}).finally(()=>{v=null,j(),Z()})}),u.addEventListener(`click`,()=>{y=!y,l.textContent=``,P()}),o.addEventListener(`submit`,e=>{e.preventDefault();let n=new FormData(o),r=o.querySelector(`button[type="submit"]`);r.disabled=!0,l.textContent=``,t(`/api/v1/auth/${y?`signup`:`login`}`,{method:`POST`,body:JSON.stringify({email:n.get(`email`),password:n.get(`password`)})}).then(e=>{v=e.account,j(),o.reset(),a.close();let t=x;x=null,t?t():Z()}).catch(e=>{l.textContent=e instanceof Error?e.message:`That did not work.`}).finally(()=>{r.disabled=!1})}),f.addEventListener(`submit`,e=>{e.preventDefault();let n=new FormData(f);h.disabled=!0,g.textContent=``;let r=String(n.get(`startsAt`)??``),i={title:n.get(`title`),description:n.get(`description`),topic:n.get(`topic`),visibility:n.get(`visibility`),chatEnabled:n.get(`chatEnabled`)===`on`,handRaiseEnabled:n.get(`handRaiseEnabled`)===`on`,recordingEnabled:n.get(`recordingEnabled`)===`on`,...b?{startsAt:new Date(r).toISOString(),timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`,expectedDurationMinutes:Number(n.get(`expectedDurationMinutes`))}:{}};t(`/api/v1/events`,{method:`POST`,body:JSON.stringify(i)}).then(async e=>{let n=e;b||(n=await t(`/api/v1/events/${encodeURIComponent(e.event.id)}/start`,{method:`POST`,body:JSON.stringify({version:e.event.version})})),d.close(),f.reset(),I(E(n.event))}).catch(e=>{g.textContent=e instanceof Error?e.message:`Could not create the event.`}).finally(()=>{h.disabled=!1})}),document.addEventListener(`click`,e=>{let t=e.target,r=t.closest(`a[data-link]`);if(r&&r.origin===location.origin){e.preventDefault(),I(`${r.pathname}${r.search}`);return}let i=t.closest(`[data-event-mode]`)?.dataset.eventMode;(i===`live`||i===`scheduled`)&&F(i),t.closest(`[data-sign-in]`)&&N(()=>void Z());let a=t.closest(`[data-close]`);a&&a.closest(`dialog`)?.close();let o=t.closest(`[data-hand]`);o&&C&&(o.disabled=!0,n(`/api/v1/events/${encodeURIComponent(C.event.id)}/hand-raises/${encodeURIComponent(o.dataset.hand??``)}`,{state:o.dataset.state},`PATCH`).then(()=>K(C.event)).finally(()=>{o.disabled=!1}))}),window.addEventListener(`popstate`,()=>void Z()),window.addEventListener(`beforeunload`,()=>{w?.stream.getTracks().forEach(e=>e.stop())}),M().then(()=>Z());