// @ts-check /// import 'https://js.boxcast.com/v3.min.js'; import { html } from "https://esm.sh/htm/react"; import styled from 'https://esm.sh/styled-components?deps=react@18'; import { createElement as h, useState, useEffect, useRef } from 'https://esm.sh/react@18'; import { createRoot } from "https://esm.sh/react-dom/client"; import State01 from "./testdata-01.json" assert { type: "json" }; import State02 from "./testdata-02.json" assert { type: "json" }; import State03 from "./testdata-03.json" assert { type: "json" }; const StyledRoot = styled.div` .Boxcast-Upper { background: white; box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.1); .Boxcast-Player { .boxcast-well-container { display: none; } .boxcast-well { display:flex; justify-content: center; align-items: center; gap: 10px 10px; min-height: 10px; margin: 0; & > span, .boxcast-linkback { display:none; } & > * { margin: 0; } } } .Boxcast-Active { text-align: center; padding:20px; background: white; & > * { margin: 0; } } } .Boxcast-Playlist { max-width: 550px; margin: 30px auto; .Partition { margin: 15px 0 0 0; padding: 0 0 8px 0; border-bottom: 1px solid #dddddd; text-align: center; } .Broadcast { position: relative; display: flex; padding: 5px 0 5px 0; & > * { box-sizing: border-box; padding: 5px; } .Pointer { width: 75px; text-align: right; .Badge { display: inline-block; border-radius: 20px; padding: 2px 8px; font-size: 12px; font-weight: 900; font-family: sans-serif; letter-spacing: 0.1em; text-transform: uppercase; text-align: center; &.Next { background: yellow; color: black; } &.Soon { background: orange; color: white; } &.Live { background: red; color: white; } } } .Time { width: 80px; font-size: 16px; text-align: right; } .Title { flex: 1; font-weight: 900; } .Control { width: 100px; } button { position: relative; appearance: none; display: block; width: 100%; padding: 5px 10px 5px 10px; background: black; cursor: pointer; border: none; color: white; font-size: 14px; font-weight: 600; transition: all 0.4s; &::before { content: " "; display: block; position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px solid transparent; outline: 0px solid red; transition: all 0.4s; } } button[disabled]::before { top: -5px; left: -5px; border: 5px solid transparent; outline: 5px solid red; } button:hover::before { outline: 5px solid red; } &.future button { background: #aaa; } @media(max-width:500px) { flex-wrap: wrap; .Time { order: 0; width: 30%; } .Title { order: 1; flex: none; width: 60%; } .Pointer { order: 2; width: 30%; } .Control { order: 3; width: 60%; } } } } .Boxcast-Alert { position: fixed; right: 20px; bottom: -300px; width: 300px; padding: 20px 0 40px 0; background: #333; border-radius: 5px; transition: bottom 0.4s; color: #fff; text-align: center; &.Show { bottom: 20px; } button { padding: 5px 15px; border: none; background: white; cursor: pointer; font-weight: 900; } .Close { display: inline-block; position: absolute; padding: 5px 10px 5px 10px; border-radius: 20px; border: 3px solid white; top: -20px; right: 10px; background: #000000; cursor: pointer; color: #fff; } } `; const PlayerID = "boxcast-player"; /** @type {(props:{channel:string, interval:number})=>any} */ const App = props => { /** @type {Boxcast.StateBinding>} */ const [ListGet, ListSet] = useState([]); /** @type {Boxcast.StateBinding} */ const [SelectedGet, SelectedSet] = useState(null); /** @type {Boxcast.StateBinding} */ const [LeadingGet, LeadingSet] = useState(null); /** @type {Boxcast.StateBinding} */ const [AlertGet, AlertSet] = useState(false); /** @type {(inList:Array)=>Array} */ const SortStart = (inList) => { inList.sort((a, b) => a.starts_at > b.starts_at ? 1 : -1); inList.forEach( item => item.start = DateParse(item.starts_at)); return inList; }; const Player = useRef(null); // on mount useEffect(()=> { Player.current = boxcast(`#${PlayerID}`); /** @type {()=>Promise} */ const Ping = async () => { const response = await fetch(`https://rest.boxcast.com/channels/${props.channel}/broadcasts?l=50`); /** @type {Array} */ const json = await response.json(); ListSet(SortStart(json)); }; Ping(); const timer = setInterval(Ping, props.interval); return ()=>clearInterval(timer); } , []); // on new list useEffect(()=> { let leading; for(let i=0; i { const settings = { selectedBroadcastId: SelectedGet, showTitle: true, showDescription: true, showCountdown: true, showRelated: false, autoplay: true, defaultVideo: "next" }; Player.current.loadChannel(props.channel, settings); } , [SelectedGet]); /** @type {(inItem:Boxcast.Broadcast)=>void} */ const SelectionTransition = (inItem) => { SelectedSet(inItem.id); document.documentElement.style.scrollBehavior = "smooth"; window.location = "#"+PlayerID; }; return html` <${StyledRoot}>

${ ListGet.filter( item=>item.id == SelectedGet )[0]?.name }

${ ListGet.map( (item, index) => { return h(BroadcastItem, { item: item, previous: ListGet[index-1], priority: item.id == LeadingGet?.id, selected: item.id == SelectedGet, select: () => SelectionTransition(item) }); }) }
{ AlertSet(false); }}>Dismiss ✕

A new session is starting:

${LeadingGet?.name}

`; } /** @type {(props:{item:Boxcast.Broadcast, previous: false | Boxcast.Broadcast, priority:boolean, selected:boolean, select:()=>void})=>any} */ const BroadcastItem = ({item, previous, priority, selected, select}) => { // pointer let pointerText; if (priority){ pointerText = html``; } if(item.timeframe == "preroll"){ pointerText = html`
Soon
`; } if(item.timeframe == "current"){ pointerText = html`
Live
`; } // (date) partition let partition; if(!previous || (previous.start.Date !== item.start.Date)) { partition = html`

${item.start.Day}, ${item.start.Month} ${item.start.Date}

`; } // button let buttonText; if(item.timeframe == "past"){ buttonText = "Rewatch"; } if(item.timeframe == "current" || item.timeframe == "preroll"){ buttonText = "Watch"; } if(item.timeframe == "future"){ buttonText = "Preview"; } return html` ${ partition }
${item.start.Hours}:${item.start.Minutes} ${item.start.M}
${item.name}
${ pointerText }
`; }; /** @type {(inDate:string)=>Boxcast.Date} */ const DateParse = (inDateString) => { let date = new Date(inDateString); /** @type {Boxcast.Date} */ let obj = { Zone: date.toString().match(/\(([A-Za-z\s].*)\)/), Day: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()], Month: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][date.getMonth()], Date: date.getDate(), Hours: date.getHours(), Minutes: date.getMinutes(), Epoch: date.valueOf() }; obj.Zone = obj.Zone ? obj.Zone[1] : "local time"; obj.M = obj.Hours >= 12 ? "PM" : "AM"; obj.Hours %= 12; if(obj.Hours == 0){ obj.Hours = 12; } if(obj.Minutes < 10){ obj.Minutes = "0"+obj.Minutes; } return obj; }; /** @type {(inChannel:string, inSelector:string, inInterval:number)=>void} */ const Init = (inChannel, inSelector, inInterval) => createRoot(document.querySelector(inSelector)).render(h(App, {channel:inChannel, interval:inInterval})); const Channel = { Basics: "sfz7ja3rlpoous6usu8a", Sunday: "gzahmhugrzogttfdtbjj", Dev: "r3os2zfdnhlquhuypgtp" }; Init(Channel.Basics, "#boxcast", 5000);