nixamp 0.27.2 → 0.27.3

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 (42) hide show
  1. package/README.md +39 -16
  2. package/backtoschool/dist/assets/{index-OE2xWZUH.css → index-Bot4FEXQ.css} +1 -1
  3. package/backtoschool/dist/assets/index-Cjai7LgT.js +93 -0
  4. package/backtoschool/dist/index.html +13 -2
  5. package/dist/event-writer.d.ts +39 -0
  6. package/dist/event-writer.js +139 -0
  7. package/dist/live-api.d.ts +2 -0
  8. package/dist/live-api.js +37 -2
  9. package/dist/live-interpreter.d.ts +3 -1
  10. package/dist/live-interpreter.js +109 -83
  11. package/dist/live-recovery.d.ts +6 -0
  12. package/dist/live-recovery.js +11 -0
  13. package/dist/mail-sender.d.ts +9 -0
  14. package/dist/mail-sender.js +11 -0
  15. package/dist/notify.d.ts +3 -3
  16. package/dist/notify.js +8 -5
  17. package/dist/password-reset.d.ts +2 -3
  18. package/dist/password-reset.js +4 -2
  19. package/dist/server.d.ts +2 -0
  20. package/dist/server.js +13 -1
  21. package/dist/shared-translation.d.ts +1 -0
  22. package/dist/shared-translation.js +32 -23
  23. package/package.json +1 -1
  24. package/src/event-writer.ts +126 -0
  25. package/src/live-api.ts +26 -2
  26. package/src/live-interpreter.ts +87 -68
  27. package/src/live-recovery.ts +10 -0
  28. package/src/mail-sender.ts +12 -0
  29. package/src/notify.ts +10 -7
  30. package/src/password-reset.ts +5 -5
  31. package/src/server.ts +14 -1
  32. package/src/shared-translation.ts +27 -19
  33. package/web/dist/assets/background.worker-Hz7oaQ8X.js +547 -0
  34. package/web/dist/assets/{hls-3VKVEQE3-CAL4qBEq.js → hls-3VKVEQE3-_BiJh76x.js} +1 -1
  35. package/web/dist/assets/index-BgLxkXKe.js +3 -0
  36. package/web/dist/assets/{mpegts-H9oC2fAK.js → mpegts-CV0glck7.js} +1 -1
  37. package/web/dist/assets/{mpegts-LO6RVLD6-Bkd9Sqr6.js → mpegts-LO6RVLD6-iuTMg-O6.js} +1 -1
  38. package/web/dist/index.html +1 -1
  39. package/web/dist/sw.js +6 -6
  40. package/backtoschool/dist/assets/index-pQ_uH4bZ.js +0 -93
  41. package/web/dist/assets/background.worker-DH8ag5Q4.js +0 -547
  42. package/web/dist/assets/index-CLeEWlms.js +0 -3
@@ -1 +1 @@
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};
1
+ import{t as e}from"./index-BgLxkXKe.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-CV0glck7.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,7 +17,7 @@
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-CLeEWlms.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-BgLxkXKe.js"></script>
21
21
  <link rel="stylesheet" crossorigin href="/assets/index-D34dTY18.css">
22
22
  </head>
23
23
  <body>
package/web/dist/sw.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1789330339689";
2
+ const CACHE = "nixamp-1789332836671";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/.well-known/openaccess.json",
6
6
  "/apple-touch-icon.png",
7
- "/assets/background.worker-DH8ag5Q4.js",
8
- "/assets/hls-3VKVEQE3-CAL4qBEq.js",
7
+ "/assets/background.worker-Hz7oaQ8X.js",
8
+ "/assets/hls-3VKVEQE3-_BiJh76x.js",
9
9
  "/assets/hls-n74Cnh8A.js",
10
- "/assets/index-CLeEWlms.js",
10
+ "/assets/index-BgLxkXKe.js",
11
11
  "/assets/index-D34dTY18.css",
12
- "/assets/mpegts-H9oC2fAK.js",
13
- "/assets/mpegts-LO6RVLD6-Bkd9Sqr6.js",
12
+ "/assets/mpegts-CV0glck7.js",
13
+ "/assets/mpegts-LO6RVLD6-iuTMg-O6.js",
14
14
  "/assets/native-C7JTKWJH-BUyIoj0P.js",
15
15
  "/favicon.png",
16
16
  "/hero.png",
@@ -1,93 +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)}})();function e(e){if(typeof e!=`string`||e.length>2048)return null;try{let t=new URL(e);return t.protocol!==`https:`||t.username||t.password?null:t.href}catch{return null}}function t(n){let r=e(n);if(!r)return null;let i=new URL(r);if(!i.port&&(i.hostname===`pairux.com`||i.hostname===`www.pairux.com`)){let e=/^\/(?:l|join|embed)\/([A-Za-z0-9_-]{1,64})\/?$/.exec(i.pathname)?.[1];return e?{provider:`pairux`,url:`https://pairux.com/l/${e}`,embed:`https://pairux.com/embed/${e}`,join:`https://pairux.com/join/${e}`}:null}if(!i.port&&(i.hostname===`nixamp.com`||i.hostname===`www.nixamp.com`)){if(i.pathname!==`/`)return null;let t=i.searchParams.get(`url`)||``,n=i.searchParams.get(`play`)||(t?`live`:``);if(/\/admin\//i.test(t)||/\/admin\//i.test(n)||i.searchParams.has(`key`)||t&&!e(t)||!/^(?:live|channel:[^\s]+|track:\d+)$/.test(n)&&!e(n))return null;let r=new URL(`https://nixamp.com/`);t&&r.searchParams.set(`url`,t),r.searchParams.set(`play`,n);let a=r.href;return r.searchParams.set(`embed`,`1`),{provider:`nixamp`,url:a,embed:r.href,join:a}}if(i.hostname.endsWith(`.nixamp.com`)&&/^\/view\/[A-Za-z0-9_-]+\/?$/.test(i.pathname)&&!i.search){let e=new URL(`https://nixamp.com/`);return e.searchParams.set(`url`,i.href),e.searchParams.set(`play`,`live`),t(e.href)}return null}var n=class extends Error{status;constructor(e,t){super(e),this.status=t}};async function r(e,t={}){let r=await fetch(e,{credentials:`same-origin`,...t,headers:{...t.body?{"content-type":`application/json`}:{},...t.headers}}),i={};try{i=await r.json()}catch{i={}}if(!r.ok)throw new n(String((i&&typeof i==`object`?i:{}).error??`That did not work.`),r.status);return i}function i(e,t,n=`POST`){return r(e,{method:n,body:JSON.stringify(t)})}var a=document.querySelector(`#main`),o=document.querySelector(`#account-button`),s=document.querySelector(`#account-dialog`),c=document.querySelector(`#account-form`),l=document.querySelector(`#account-title`),u=document.querySelector(`#account-copy`),d=document.querySelector(`#account-error`),f=document.querySelector(`#account-mode`),p=document.querySelector(`#event-dialog`),m=document.querySelector(`#event-form`),h=document.querySelector(`#event-form-title`),g=document.querySelector(`#event-kicker`),_=document.querySelector(`#event-submit`),v=document.querySelector(`#event-error`),y=document.querySelector(`#schedule-fields`),b=null,x=!1,S=!1,C=null,w=!1,T=null,E=null,D=null,O=new WeakMap;function k(e,t){O.get(e)===t||e.contains(document.activeElement)||(e.innerHTML=t,O.set(e,t))}function A(e){return String(e??``).replaceAll(`&`,`&amp;`).replaceAll(`<`,`&lt;`).replaceAll(`>`,`&gt;`).replaceAll(`"`,`&quot;`).replaceAll(`'`,`&#039;`)}function j(e){let t=document.querySelector(`#event-update`);t&&t.textContent!==e&&(t.textContent=e),document.querySelector(`.class-update`)?.removeAttribute(`hidden`)}function M(e){return`/live/${encodeURIComponent(e.slug)}`}function N(){let e=new URLSearchParams(location.search).get(`invite`);return e?`?invite=${encodeURIComponent(e)}`:``}function P(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 F(e){return e.status===`live`?`Live now`:e.status===`ended`?e.recordingId?`Replay available`:`Event ended`:e.status===`cancelled`?`Cancelled`:P(e.startsAt)}function I(e){return D?.permissions.includes(e)??!1}function L(){o.textContent=b?b.email.split(`@`)[0]||`Account`:`Sign in`,o.classList.toggle(`signed-in`,!!b)}async function R(){try{b=(await r(`/api/v1/auth/me`)).account}catch{b=null}L()}function z(e){if(b){s.showModal(),l.textContent=`Signed in as ${b.email}`,u.textContent=`Your BackToSchool identity is your NixAmp account.`,c.hidden=!0;return}T=e??null,c.hidden=!1,d.textContent=``,B(),s.showModal()}function B(){l.textContent=x?`Create your account`:`Welcome back`,u.textContent=x?`One NixAmp account works here and everywhere NixAmp goes.`:`Sign in to host, chat, or raise your hand.`,f.textContent=x?`Already have an account? Sign in`:`New here? Create an account`,document.querySelector(`#account-forgot`).hidden=x;let e=c.elements.namedItem(`password`);e.autocomplete=x?`new-password`:`current-password`,c.querySelector(`button[type="submit"]`).textContent=x?`Create account`:`Sign in`}function V(e){let t=new Date(e);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,`0`)}-${String(t.getDate()).padStart(2,`0`)}T${String(t.getHours()).padStart(2,`0`)}:${String(t.getMinutes()).padStart(2,`0`)}`}function H(e,t=null){if(!b){z(()=>H(e,t));return}if(w)return;C=t,m.reset(),S=e===`scheduled`,y.hidden=!S,h.textContent=t?`Edit event`:S?`Schedule a class`:`Go live`,g.textContent=t?`Your classroom`:`Teach something live`,_.textContent=t?`Save changes`:S?`Schedule class`:`Create live`,v.textContent=``;let n=m.elements.namedItem(`startsAt`);if(n.required=S&&(!t||!!t.startsAt),t){for(let e of[`title`,`description`,`topic`,`visibility`,`broadcastUrl`,`hostName`,`homepageUrl`,`avatarUrl`,`recurrence`])m.elements.namedItem(e).value=t[e]??(e===`recurrence`?`none`:``);for(let e of[`chatEnabled`,`handRaiseEnabled`])m.elements.namedItem(e).checked=t[e];t.startsAt&&(n.value=V(t.startsAt));let e=m.elements.namedItem(`expectedDurationMinutes`),r=String(t.expectedDurationMinutes??60);[...e.options].some(e=>e.value===r)||e.add(new Option(`${r} minutes`,r)),e.value=r}else if(S){let e=new Date(Date.now()+36e5);e.setMinutes(Math.ceil(e.getMinutes()/15)*15,0,0),n.value=V(e.toISOString())}let r=Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`;document.querySelector(`#schedule-timezone`).textContent=`Date and time are shown in ${r}.`+(t?.recurrence?` This class repeats in ${t.timezone}.`:``),p.showModal()}function U(e){history.pushState(null,``,e),$()}function W(e){let t=e.status===`live`||e.status===`starting`;return`
2
- <article class="event-card ${t?`is-live`:``}">
3
- <a href="${M(e)}" data-link>
4
- <div class="event-card-top">
5
- <span class="status-pill ${t?`status-live`:``}">${t?`Live`:A(F(e))}</span>
6
- ${e.topic?`<span class="topic">${A(e.topic)}</span>`:``}
7
- </div>
8
- <h3>${A(e.title)}</h3>
9
- <p>${A(e.description||`Drop in and learn something with us.`)}</p>
10
- <span class="event-link">${t?`Watch live`:`See event`}<span aria-hidden="true">→</span></span>
11
- </a>
12
- </article>`}async function G(){document.title=`BackToSchool.help — Learn something live`;let{events:e}=await r(`/api/v1/events?limit=60`).catch(()=>({events:[]})),t=e.filter(e=>e.status===`live`||e.status===`starting`),n=e.filter(e=>e.status===`scheduled`||e.status===`draft`),i=e.filter(e=>e.status===`ended`&&e.recordingId);a.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>Watch a live lesson, follow a screen share, and ask questions as you learn.</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">Watch classes and screen shares. 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>${A(t[0]?.title??n[0]?.title??`The best teachers are curious people`)}</strong>
30
- <p>${t[0]?`Happening right now`:n[0]?F(n[0]):`Host the first live conversation`}</p>
31
- ${t[0]?`<a href="${M(t[0])}" data-link class="board-action">Watch 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">${t.length} ${t.length===1?`room`:`rooms`}</span>
40
- </div>
41
- <div class="event-grid">
42
- ${t.length?t.map(W).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
- ${n.length?n.map(e=>`
52
- <a class="upcoming-row" href="${M(e)}" data-link>
53
- <time>${A(P(e.startsAt,{weekday:`short`,month:`short`,day:`numeric`}))}</time>
54
- <span><strong>${A(e.title)}</strong><small>${A(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
- ${i.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">${i.map(W).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 K(e){let n=t(e.broadcastUrl);if(n)return`<div class="classroom-video"><iframe src="${A(n.embed)}" title="${A(e.title)} broadcast" allow="autoplay; fullscreen; picture-in-picture; encrypted-media" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></div>
68
- <p class="broadcast-caption"><a class="button button-secondary" href="${A(n.join)}" target="_blank" rel="noopener noreferrer">${n.provider===`pairux`?`Join screen share on Pairux`:`Open video in Nixamp`} ↗</a></p>`;let r=N(),i=e.recordingId&&e.status===`ended`?`/api/v1/recordings/${encodeURIComponent(e.recordingId)}`:`/api/channels/${encodeURIComponent(e.roomId)}${r}`,a=e.status===`live`||!!e.recordingId;return`
69
- <div class="listen-card ${a?``:`waiting`}">
70
- <div class="sound-orbit" aria-hidden="true"><span></span><span></span><span></span><b>▶</b></div>
71
- <div>
72
- <span class="eyebrow">${e.status===`live`?`On air now`:e.recordingId?`Replay`:`The room opens soon`}</span>
73
- <h2>${a?e.status===`live`?`Listen live`:`Listen again`:`We’ll see you here.`}</h2>
74
- <p id="player-note">${a?`Press play and you’re in. No sign-up, no setup.`:A(F(e))}</p>
75
- ${a?`<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="${A(i)}" controls></audio>`:``}
76
- </div>
77
- </div>`}function q(e){return e.chatEnabled?`
78
- <div id="chat-messages" class="chat-messages"><p class="muted">Loading the conversation…</p></div>
79
- ${b?`<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 J(e,t){switch(e){case`event-header`:return`<div class="event-heading"><span class="status-pill ${t.status===`live`?`status-live`:``}">${A(F(t))}</span>${t.topic?`<span class="topic">${A(t.topic)}</span>`:``}<h1>${A(t.title)}</h1><p>${A(t.description||`A live conversation powered by NixAmp.`)}</p></div>`;case`player`:return K(t);case`stage`:return`<div class="stage-card"><span class="stage-avatar">${A(t.title.slice(0,1).toUpperCase())}</span><div><span class="eyebrow">Host stage</span><h2>${A(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">${t.avatarUrl?`<img class="avatar host-avatar" src="${A(t.avatarUrl)}" alt="" referrerpolicy="no-referrer" />`:`<span class="avatar">${A((t.hostName||t.title).slice(0,1))}</span>`}<div><small>Your host</small><strong>${A(t.hostName||`Class host`)}</strong>${t.homepageUrl?`<a href="${A(t.homepageUrl)}" target="_blank" rel="noopener noreferrer">Visit homepage ↗</a>`:``}</div></div>`;case`about`:return`<p class="reading-copy">${A(t.description||`Come listen, learn, and ask a question live.`)}</p>${t.topic?`<span class="topic topic-large">${A(t.topic)}</span>`:``}`;case`join`:return b?`<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 q(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="${A(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">
80
- <a href="https://pairux.com/dashboard" target="_blank" rel="noopener noreferrer">Screen share with Pairux ↗</a>
81
- <a href="https://nixamp.com/#files-panel" target="_blank" rel="noopener noreferrer">Upload &amp; broadcast in Nixamp ↗</a>
82
- ${[`draft`,`scheduled`,`starting`].includes(t.status)?`<button class="button button-primary" type="button" data-start-event>Start class</button>`:``}
83
- <button class="button button-danger" type="button" data-end-event ${t.status===`live`?``:`hidden`}>${t.recurrence?`End session & schedule next`:`End event`}</button>
84
- <p id="broadcast-note" class="form-note" role="status">${t.broadcastUrl?`Start the broadcast in Pairux or Nixamp, then start the class here.`:`Add your broadcast link with Edit event.`}</p>
85
- </div>`;case`schedule`:return`<div class="metric"><strong>${A(P(t.startsAt,{timeZone:t.timezone}))}</strong><span>${A(t.timezone)}${t.recurrence?` · Repeats ${t.recurrence}`:``}</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}}async function Y(e){let t=await r(`/api/v1/events/${encodeURIComponent(e)}${N()}`);E?.(),E=null,D=t;let n=t.event;document.title=`${n.title} — BackToSchool.help`,document.querySelector(`meta[name="description"]`)?.setAttribute(`content`,n.description||`Listen to ${n.title} live on BackToSchool.help.`),document.querySelector(`meta[property="og:title"]`)?.setAttribute(`content`,n.title),document.querySelector(`meta[property="og:description"]`)?.setAttribute(`content`,n.description||`Learn something live.`);let i=(e,t)=>`<section class="event-panel panel-${e}"><h2 class="panel-title">${t}</h2>${J(e,n)??``}</section>`;a.innerHTML=`
86
- <div class="event-shell">
87
- <div class="event-toolbar"><a class="back-link" href="/" data-link>← Explore more lives</a>${b?.id===n.ownerId?`<button class="button button-secondary" type="button" data-edit-event>Edit event</button>`:``}</div>
88
- <div class="class-update" hidden><p id="event-update" role="status" class="form-note"></p><button class="text-button" data-refresh-event>Refresh class details</button><button class="text-button" data-dismiss-update>Dismiss</button></div>
89
- <div class="event-layout">
90
- <div class="region region-primary">${i(`event-header`,`Live classroom`)}${i(`player`,`Watch class`)}${i(`chat`,`Class chat`)}</div>
91
- <aside class="region region-secondary">${i(`host`,`Meet your host`)}${i(`schedule`,`Schedule`)}${b?.id===n.ownerId?i(`event-controls`,`Host controls`):i(`join`,`Join the class`)}${b?.id===n.ownerId&&n.visibility===`private`?`<details class="event-panel"><summary>Invite students</summary>${J(`invite`,n)}</details>`:``}${i(`share`,`Share class`)}${b?.id===n.ownerId&&n.handRaiseEnabled?i(`hand-raises`,`Raised hands`):n.handRaiseEnabled?i(`raise-hand`,`Ask a question`):``}</aside>
92
- </div>
93
- </div>`,X(n)}function X(e){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 t=document.querySelector(`#chat-form`);t?.addEventListener(`submit`,n=>{n.preventDefault();let r=t.elements.namedItem(`body`),a=r.value.trim();a&&(r.value=``,i(`/api/v1/events/${encodeURIComponent(e.id)}/chat`,{body:a}).then(()=>Z(e)).catch(e=>{r.placeholder=e instanceof Error?e.message:`Could not send`}))});let a=document.querySelector(`#invite-form`);a?.addEventListener(`submit`,t=>{t.preventDefault();let n=new FormData(a),i=document.querySelector(`#invite-note`);i.textContent=`Creating invite…`,r(`/api/v1/events/${encodeURIComponent(e.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 r=t.currentTarget;r.disabled=!0,i(`/api/v1/events/${encodeURIComponent(e.id)}/hand-raises`,{}).then(()=>{r.textContent=`Hand raised ✓`}).catch(e=>{r.disabled=!1,r.textContent=e instanceof n&&e.status===401?`Sign in to raise hand`:`Try again`})}),document.querySelector(`[data-edit-event]`)?.addEventListener(`click`,()=>H(`scheduled`,D.event)),document.querySelector(`[data-start-event]`)?.addEventListener(`click`,()=>void te()),document.querySelector(`[data-dismiss-update]`)?.addEventListener(`click`,()=>document.querySelector(`.class-update`).hidden=!0),document.querySelector(`[data-refresh-event]`)?.addEventListener(`click`,()=>U(M(e)+N())),document.querySelector(`[data-end-event]`)?.addEventListener(`click`,()=>void ne()),document.querySelector(`[data-copy]`)?.addEventListener(`click`,async e=>{await navigator.clipboard.writeText(location.href),e.currentTarget.textContent=`Copied ✓`});let o=0,s=0;e.chatEnabled&&(Z(e),o=window.setInterval(()=>void Z(e),4e3),s=I(`event.moderate`)?window.setInterval(()=>void Q(e),3e3):0,Q(e));let c=e.version,l=window.setInterval(()=>void r(`/api/v1/events/${encodeURIComponent(e.id)}${N()}`).then(e=>{if(e.event.version===D?.event.version||e.event.version===c)return;c=e.event.version;let t=document.querySelector(`.event-heading .status-pill`),n=F(e.event);t&&t.textContent!==n&&(t.textContent=n,t.classList.toggle(`status-live`,e.event.status===`live`)),j(`The host updated this class. Refresh the details when you’re ready.`)}).catch(()=>void 0),5e3);E=()=>{clearInterval(o),clearInterval(s),clearInterval(l)},ee(e)}async function Z(e){let t=document.querySelector(`#chat-messages`);if(t)try{let{messages:n}=await r(`/api/v1/events/${encodeURIComponent(e.id)}/chat${N()}`);k(t,n.length?n.map(e=>`<div class="chat-message"><span class="avatar avatar-small">${A(e.authorName.slice(0,1).toUpperCase())}</span><p><strong>${A(e.authorName)}</strong><span>${A(e.body)}</span></p></div>`).join(``):`<p class="muted">No messages yet. Say hello when you’re ready.</p>`)}catch{t.innerHTML=`<p class="muted">Chat is taking a break. You can still watch the class.</p>`}}async function Q(e){let t=document.querySelector(`#hand-raises`);if(t&&I(`event.moderate`))try{let{handRaises:n}=await r(`/api/v1/events/${encodeURIComponent(e.id)}/hand-raises`);if(t.contains(document.activeElement))return;k(t,n.length?n.map(e=>`<div class="raise-row"><span><strong>${A(e.displayName)}</strong><small>${A(e.state)}</small></span><div><button class="text-button" data-hand="${A(e.accountId)}" data-state="invited">Invite</button><button class="text-button" data-hand="${A(e.accountId)}" data-state="dismissed">Dismiss</button></div></div>`).join(``):`<p class="muted">No hands raised.</p>`)}catch{t.contains(document.activeElement)||(t.innerHTML=`<p class="muted">Could not refresh hand raises.</p>`)}}async function ee(e){let t=document.querySelector(`#listener-count`);if(t)try{let{channels:n}=await r(`/api/channels`);t.textContent=String(n.find(t=>t.id===e.roomId)?.listeners??0)}catch{t.textContent=`0`}}async function te(){if(!D)return;let e=document.querySelector(`#broadcast-note`),t=document.querySelector(`[data-start-event]`);if(!D.event.broadcastUrl){e&&(e.textContent=`Add a Pairux or Nixamp broadcast link with Edit event first.`);return}if(t?.getAttribute(`aria-disabled`)!==`true`){t?.setAttribute(`aria-disabled`,`true`);try{D=await r(`/api/v1/events/${encodeURIComponent(D.event.id)}/start`,{method:`POST`,body:JSON.stringify({version:D.event.version})}),t&&(t.textContent=`Class is live`),document.querySelector(`[data-end-event]`)?.removeAttribute(`hidden`);let n=document.querySelector(`.event-heading .status-pill`);n&&(n.textContent=`Live now`,n.classList.add(`status-live`)),e&&(e.textContent=`Your class is live. Viewers can join using this page.`)}catch(n){t?.removeAttribute(`aria-disabled`),e&&(e.textContent=n instanceof Error?n.message:`Could not start the class.`)}}}async function ne(){let e=document.querySelector(`[data-end-event]`);if(!D||e?.getAttribute(`aria-disabled`)===`true`)return;e?.setAttribute(`aria-disabled`,`true`);let t=D.event;try{let e=await r(`/api/v1/events/${encodeURIComponent(t.id)}/end`,{method:`POST`,body:JSON.stringify({version:t.version})});D=e;let n=document.querySelector(`.event-heading .status-pill`);n&&(n.textContent=F(e.event),n.classList.remove(`status-live`)),await fetch(`/api/channels/${encodeURIComponent(t.roomId)}`,{method:`DELETE`,credentials:`same-origin`});let i=document.querySelector(`#broadcast-note`);i&&(i.textContent=e.event.recurrence?`Session ended. Next class: ${P(e.event.startsAt)}. End the broadcast in Pairux or Nixamp too.`:`Event ended. End the broadcast in Pairux or Nixamp too.`);let a=document.querySelector(`[data-end-event]`);a&&(a.textContent=`Session ended`,a.setAttribute(`aria-disabled`,`true`)),j(`This session has ended. Refresh to see the latest schedule.`)}catch(t){e?.removeAttribute(`aria-disabled`);let n=document.querySelector(`#broadcast-note`);n&&(n.textContent=t instanceof Error?t.message:`Could not end the event.`)}}async function $(){E?.(),E=null,D=null;let e=/^\/live\/([^/]+)\/?$/.exec(location.pathname);try{e?await Y(decodeURIComponent(e[1])):await G()}catch(e){a.innerHTML=`<section class="error-page"><span class="eyebrow">Class dismissed?</span><h1>${A(e instanceof n&&e.status===404?`That live event isn’t available.`:`We couldn’t open this page.`)}</h1><p>${A(e instanceof Error?e.message:`Try again in a moment.`)}</p><a href="/" data-link class="button button-primary">Back to live events</a></section>`}}o.addEventListener(`click`,()=>{if(!b){z();return}confirm(`Signed in as ${b.email}. Sign out?`)&&r(`/api/v1/auth/logout`,{method:`POST`}).finally(()=>{b=null,L(),$()})}),f.addEventListener(`click`,()=>{x=!x,d.textContent=``,B()}),c.addEventListener(`submit`,e=>{e.preventDefault();let t=new FormData(c),n=c.querySelector(`button[type="submit"]`);n.disabled=!0,d.textContent=``,r(`/api/v1/auth/${x?`signup`:`login`}`,{method:`POST`,body:JSON.stringify({email:t.get(`email`),password:t.get(`password`)})}).then(e=>{b=e.account,L(),c.reset(),s.close();let t=T;T=null,t?t():$()}).catch(e=>{d.textContent=e instanceof Error?e.message:`That did not work.`}).finally(()=>{n.disabled=!1})}),m.querySelector(`[name="recurrence"]`)?.addEventListener(`change`,()=>{m.elements.namedItem(`recurrence`).value!==`none`&&(S=!0,y.hidden=!1,m.elements.namedItem(`startsAt`).required=!0)}),m.addEventListener(`submit`,e=>{if(e.preventDefault(),w)return;let t=new FormData(m),n=C,i=String(t.get(`startsAt`)??``),a=!n?.startsAt||i!==V(n.startsAt),o=String(t.get(`recurrence`));if(o!==`none`&&!i&&!S){S=!0,y.hidden=!1,m.elements.namedItem(`startsAt`).required=!0,v.textContent=`Choose the first class date and time for this recurring schedule.`;return}w=!0,_.setAttribute(`aria-disabled`,`true`),m.setAttribute(`aria-busy`,`true`),v.textContent=``;let s={title:t.get(`title`),description:t.get(`description`),topic:t.get(`topic`),visibility:t.get(`visibility`),broadcastUrl:t.get(`broadcastUrl`),hostName:t.get(`hostName`),homepageUrl:t.get(`homepageUrl`),avatarUrl:t.get(`avatarUrl`),recurrence:o,chatEnabled:t.get(`chatEnabled`)===`on`,handRaiseEnabled:t.get(`handRaiseEnabled`)===`on`,...n?{version:n.version}:{kind:`class`},...S?{startsAt:i?a?new Date(i).toISOString():n.startsAt:null,timezone:a?Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`:n.timezone,expectedDurationMinutes:Number(t.get(`expectedDurationMinutes`))}:{}};r(n?`/api/v1/events/${encodeURIComponent(n.id)}`:`/api/v1/events`,{method:n?`PATCH`:`POST`,body:JSON.stringify(s)}).then(async e=>{n?(await Y(e.event.slug),p.close(),document.querySelector(`[data-edit-event]`)?.focus({preventScroll:!0})):(p.close(),U(M(e.event))),m.reset(),C=null}).catch(e=>{v.textContent=e instanceof Error?e.message:`Could not save the event.`}).finally(()=>{w=!1,_.removeAttribute(`aria-disabled`),m.removeAttribute(`aria-busy`)})}),document.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`a[data-link]`);if(n&&n.origin===location.origin){e.preventDefault(),U(`${n.pathname}${n.search}`);return}let r=t.closest(`[data-event-mode]`)?.dataset.eventMode;(r===`live`||r===`scheduled`)&&H(r),t.closest(`[data-sign-in]`)&&z(()=>void $());let a=t.closest(`[data-close]`);a&&a.closest(`dialog`)?.close();let o=t.closest(`[data-hand]`);o&&D&&(o.disabled=!0,i(`/api/v1/events/${encodeURIComponent(D.event.id)}/hand-raises/${encodeURIComponent(o.dataset.hand??``)}`,{state:o.dataset.state},`PATCH`).then(()=>Q(D.event)).finally(()=>{o.disabled=!1}))}),window.addEventListener(`popstate`,()=>void $()),R().then(()=>$());