2022-04-22 16:56:41 -04:00
// @ts-check
/// <reference path="./types.d.ts"/>
2022-04-25 17:23:23 -04:00
import 'https://js.boxcast.com/v3.min.js' ;
2022-04-26 13:49:43 -04:00
import { html } from "https://esm.sh/htm/react" ;
2022-04-26 17:10:25 -04:00
import styled from 'https://esm.sh/styled-components?deps=react@18' ;
import { createElement as h , useState , useEffect , useRef } from 'https://esm.sh/react@18' ;
2022-04-26 13:49:43 -04:00
import { createRoot } from "https://esm.sh/react-dom/client" ;
2022-04-28 10:00:31 -04:00
2022-04-27 12:23:06 -04:00
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" } ;
2022-04-28 10:00:31 -04:00
2022-04-26 13:49:43 -04:00
const StyledRoot = styled . div `
2022-04-27 12:23:06 -04:00
. Boxcast - Upper
{
background : white ;
box - shadow : 0 px 5 px 10 px rgba ( 0 , 0 , 0 , 0.1 ) ;
2022-04-28 10:00:31 -04:00
. Boxcast - Player
2022-04-26 18:01:57 -04:00
{
2022-04-28 10:00:31 -04:00
. boxcast - well - container
2022-04-26 18:01:57 -04:00
{
2022-04-28 10:00:31 -04:00
display : none ;
2022-04-26 18:01:57 -04:00
}
2022-04-28 10:00:31 -04:00
. boxcast - well
2022-04-26 18:01:57 -04:00
{
2022-04-28 10:00:31 -04:00
display : flex ;
justify - content : center ;
align - items : center ;
gap : 10 px 10 px ;
min - height : 10 px ;
2022-04-26 18:01:57 -04:00
margin : 0 ;
2022-04-28 10:00:31 -04:00
& > span , . boxcast - linkback
{
display : none ;
}
& > *
{
margin : 0 ;
}
2022-04-26 18:01:57 -04:00
}
}
2022-04-28 10:00:31 -04:00
. Boxcast - Active
{
text - align : center ;
padding : 20 px ;
background : white ;
& > * { margin : 0 ; }
}
2022-04-26 18:01:57 -04:00
}
2022-04-28 10:00:31 -04:00
2022-04-26 18:01:57 -04:00
. Boxcast - Playlist
{
max - width : 550 px ;
margin : 30 px auto ;
2022-04-26 16:00:04 -04:00
2022-04-28 10:00:31 -04:00
. Partition
2022-04-27 12:23:06 -04:00
{
2022-04-28 10:00:31 -04:00
margin : 15 px 0 0 0 ;
padding : 0 0 8 px 0 ;
border - bottom : 1 px solid # dddddd ;
text - align : center ;
2022-04-27 12:23:06 -04:00
}
2022-04-28 10:00:31 -04:00
. Broadcast
2022-04-26 16:00:04 -04:00
{
2022-04-28 10:00:31 -04:00
position : relative ;
display : flex ;
padding : 5 px 0 5 px 0 ;
& > *
2022-04-26 16:00:04 -04:00
{
2022-04-28 10:00:31 -04:00
box - sizing : border - box ;
padding : 5 px ;
}
2022-04-26 16:00:04 -04:00
2022-04-28 10:00:31 -04:00
. Pointer
{
width : 75 px ;
text - align : right ;
. Badge
2022-04-26 16:00:04 -04:00
{
2022-04-28 10:00:31 -04:00
display : inline - block ;
border - radius : 20 px ;
padding : 2 px 8 px ;
font - size : 12 px ;
font - weight : 900 ;
font - family : sans - serif ;
letter - spacing : 0.1 em ;
text - transform : uppercase ;
text - align : center ;
& . Next
{
background : yellow ;
color : black ;
}
& . Soon
{
background : orange ;
color : white ;
}
& . Live
{
background : red ;
color : white ;
}
2022-04-26 16:00:04 -04:00
}
}
2022-04-27 12:23:06 -04:00
. Time
{
2022-04-28 10:00:31 -04:00
width : 80 px ;
font - size : 16 px ;
text - align : right ;
2022-04-27 12:23:06 -04:00
}
. Title
{
2022-04-28 10:00:31 -04:00
flex : 1 ;
font - weight : 900 ;
2022-04-27 12:23:06 -04:00
}
2022-04-28 10:00:31 -04:00
. Control
2022-04-27 08:25:22 -04:00
{
2022-04-28 10:00:31 -04:00
width : 100 px ;
2022-04-27 12:23:06 -04:00
}
2022-04-28 10:00:31 -04:00
button
{
position : relative ;
appearance : none ;
display : block ;
width : 100 % ;
padding : 5 px 10 px 5 px 10 px ;
background : black ;
cursor : pointer ;
border : none ;
color : white ;
font - size : 14 px ;
font - weight : 600 ;
transition : all 0.4 s ;
& : : before
{
content : " " ;
display : block ;
position : absolute ;
top : 0 px ;
left : 0 px ;
width : 100 % ;
height : 100 % ;
border : 0 px solid transparent ;
outline : 0 px solid red ;
transition : all 0.4 s ;
}
}
button [ disabled ] : : before
{
top : - 5 px ;
left : - 5 px ;
border : 5 px solid transparent ;
outline : 5 px solid red ;
}
button : hover : : before
2022-04-27 12:23:06 -04:00
{
2022-04-28 10:00:31 -04:00
outline : 5 px solid red ;
}
& . future button
{
background : # aaa ;
}
@ media ( max - width : 500 px )
{
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 % ;
}
2022-04-27 08:25:22 -04:00
}
}
2022-04-26 13:49:43 -04:00
}
2022-04-28 10:00:31 -04:00
. Boxcast - Alert
2022-04-26 13:49:43 -04:00
{
position : fixed ;
right : 20 px ;
bottom : - 300 px ;
width : 300 px ;
2022-04-27 12:23:06 -04:00
padding : 20 px 0 40 px 0 ;
2022-04-26 13:49:43 -04:00
background : # 333 ;
border - radius : 5 px ;
transition : bottom 0.4 s ;
color : # fff ;
text - align : center ;
& . Show
2022-04-25 17:23:23 -04:00
{
2022-04-26 13:49:43 -04:00
bottom : 20 px ;
}
2022-04-27 12:23:06 -04:00
button
{
padding : 5 px 15 px ;
border : none ;
background : white ;
cursor : pointer ;
font - weight : 900 ;
}
2022-04-26 13:49:43 -04:00
. Close
2022-04-25 17:23:23 -04:00
{
2022-04-26 13:49:43 -04:00
display : inline - block ;
position : absolute ;
padding : 5 px 10 px 5 px 10 px ;
border - radius : 20 px ;
2022-04-27 12:23:06 -04:00
border : 3 px solid white ;
top : - 20 px ;
2022-04-26 13:49:43 -04:00
right : 10 px ;
background : # 000000 ;
cursor : pointer ;
color : # fff ;
2022-04-25 17:23:23 -04:00
}
2022-04-26 13:49:43 -04:00
}
` ;
const PlayerID = "boxcast-player" ;
2022-04-22 16:56:41 -04:00
/** @type {(props:{channel:string, interval:number})=>any} */
const App = props =>
{
/** @type {Boxcast.StateBinding<Array<Boxcast.Broadcast>>} */
const [ ListGet , ListSet ] = useState ( [ ] ) ;
2022-04-26 16:00:04 -04:00
/** @type {Boxcast.StateBinding<string|null>} */
2022-04-26 10:47:45 -04:00
const [ SelectedGet , SelectedSet ] = useState ( null ) ;
2022-04-22 16:56:41 -04:00
2022-04-26 10:47:45 -04:00
/** @type {Boxcast.StateBinding<Boxcast.Broadcast|null>} */
2022-04-26 12:04:39 -04:00
const [ LeadingGet , LeadingSet ] = useState ( null ) ;
/** @type {Boxcast.StateBinding<boolean>} */
const [ AlertGet , AlertSet ] = useState ( false ) ;
2022-04-22 17:19:25 -04:00
2022-04-25 17:23:23 -04:00
/** @type {(inList:Array<Boxcast.Broadcast>)=>Array<Boxcast.Broadcast>} */
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 ;
2022-04-27 12:23:06 -04:00
} ;
2022-04-22 16:56:41 -04:00
2022-04-25 17:23:23 -04:00
const Player = useRef ( null ) ;
2022-04-22 16:56:41 -04:00
2022-04-25 17:23:23 -04:00
// on mount
2022-04-22 17:19:25 -04:00
useEffect ( ( ) =>
{
2022-04-25 17:23:23 -04:00
Player . current = boxcast ( ` # ${ PlayerID } ` ) ;
2022-04-25 10:36:55 -04:00
/** @type {()=>Promise} */
const Ping = async ( ) =>
{
const response = await fetch ( ` https://rest.boxcast.com/channels/ ${ props . channel } /broadcasts?l=50 ` ) ;
/** @type {Array<Boxcast.Broadcast>} */
const json = await response . json ( ) ;
2022-04-25 17:23:23 -04:00
ListSet ( SortStart ( json ) ) ;
2022-04-25 10:36:55 -04:00
} ;
2022-04-22 17:19:25 -04:00
Ping ( ) ;
const timer = setInterval ( Ping , props . interval ) ;
return ( ) => clearInterval ( timer ) ;
}
, [ ] ) ;
2022-04-26 17:10:25 -04:00
// on new list
useEffect ( ( ) =>
{
let leading ;
for ( let i = 0 ; i < ListGet . length ; i ++ )
{
if ( ListGet [ i ] . timeframe != "past" )
{
leading = ListGet [ i ] ;
2022-04-26 12:04:39 -04:00
2022-04-26 17:10:25 -04:00
if ( ( leading . timeframe == "current" || leading . timeframe == "preroll" ) && ( leading . id != LeadingGet ? . id ) && ( SelectedGet != leading . id ) ) // if something is selected other than the leading event, alert the user
{
AlertSet ( true ) ;
}
2022-04-26 12:04:39 -04:00
2022-04-26 17:10:25 -04:00
if ( SelectedGet == null ) // if nothing is selected select the leading event
{
SelectedSet ( leading . id ) ;
}
LeadingSet ( leading ) ;
return ;
}
}
2022-04-26 12:04:39 -04:00
if ( ListGet . length ) // if there are events but theres no leading event, clear leading and select the first event
2022-04-25 10:36:55 -04:00
{
2022-04-26 12:04:39 -04:00
LeadingSet ( null ) ;
2022-04-26 16:00:04 -04:00
if ( SelectedGet == null )
{
SelectedSet ( ListGet [ 0 ] . id ) ;
}
2022-04-26 12:04:39 -04:00
AlertSet ( false ) ;
2022-04-25 10:36:55 -04:00
}
}
, [ ListGet ] ) ;
2022-04-25 17:23:23 -04:00
// on new video selected
useEffect ( ( ) =>
{
2022-04-26 12:04:39 -04:00
const settings = {
2022-04-26 16:00:04 -04:00
selectedBroadcastId : SelectedGet ,
2022-04-25 17:23:23 -04:00
showTitle : true ,
showDescription : true ,
showCountdown : true ,
showRelated : false ,
autoplay : true ,
defaultVideo : "next"
2022-04-26 12:04:39 -04:00
} ;
Player . current . loadChannel ( props . channel , settings ) ;
2022-04-22 16:56:41 -04:00
}
2022-04-26 16:00:04 -04:00
, [ SelectedGet ] ) ;
2022-04-25 17:23:23 -04:00
2022-04-28 10:00:31 -04:00
/** @type {(inItem:Boxcast.Broadcast)=>void} */
const SelectionTransition = ( inItem ) =>
{
SelectedSet ( inItem . id ) ;
document . documentElement . style . scrollBehavior = "smooth" ;
window . location = "#" + PlayerID ;
} ;
2022-04-25 17:23:23 -04:00
return html `
2022-04-26 13:49:43 -04:00
< $ { StyledRoot } >
2022-04-27 12:23:06 -04:00
< div class = "Boxcast-Upper" >
< div class = "Boxcast-Player" id = $ { PlayerID } > < / d i v >
2022-04-28 10:00:31 -04:00
2022-04-27 12:23:06 -04:00
< div >
< button onClick = $ { ( ) => ListSet ( SortStart ( State01 ) ) } > testdata - 01 < / b u t t o n >
< button onClick = $ { ( ) => ListSet ( SortStart ( State02 ) ) } > testdata - 02 < / b u t t o n >
< button onClick = $ { ( ) => ListSet ( SortStart ( State03 ) ) } > testdata - 03 < / b u t t o n >
< / d i v >
2022-04-28 10:00:31 -04:00
2022-04-27 12:23:06 -04:00
< div class = "Boxcast-Active" >
< h2 > $ { ListGet . filter ( item => item . id == SelectedGet ) [ 0 ] ? . name } < / h 2 >
< / d i v >
< / d i v >
2022-04-25 17:23:23 -04:00
< div class = "Boxcast-Playlist" >
$ {
2022-04-26 16:00:04 -04:00
ListGet . map ( ( item , index ) =>
{
return h ( BroadcastItem ,
2022-04-25 17:23:23 -04:00
{
item : item ,
previous : ListGet [ index - 1 ] ,
2022-04-26 12:04:39 -04:00
priority : item . id == LeadingGet ? . id ,
2022-04-26 16:00:04 -04:00
selected : item . id == SelectedGet ,
2022-04-28 10:00:31 -04:00
select : ( ) => SelectionTransition ( item )
2022-04-26 16:00:04 -04:00
} ) ;
} )
2022-04-25 17:23:23 -04:00
}
< / d i v >
2022-04-28 10:00:31 -04:00
< div class = $ { ` Boxcast-Alert ${ AlertGet ? " Show" : null } ` } >
2022-04-26 13:49:43 -04:00
< span class = "Close" onClick = $ { ( ) => { AlertSet ( false ) ; } } > Dismiss ✕ < / s p a n >
< h4 > A new session is starting : < / h 4 >
< p > $ { LeadingGet ? . name } < / p >
2022-04-28 10:00:31 -04:00
< button onClick = $ { ( ) => { SelectionTransition ( LeadingGet ) ; AlertSet ( false ) ; } } > Watch Now < / b u t t o n >
2022-04-26 13:49:43 -04:00
< / d i v >
< //>
2022-04-22 16:56:41 -04:00
` ;
}
2022-04-25 17:23:23 -04:00
/** @type {(props:{item:Boxcast.Broadcast, previous: false | Boxcast.Broadcast, priority:boolean, selected:boolean, select:()=>void})=>any} */
const BroadcastItem = ( { item , previous , priority , selected , select } ) =>
2022-04-22 16:56:41 -04:00
{
2022-04-26 18:01:57 -04:00
// pointer
2022-04-25 17:23:23 -04:00
let pointerText ;
2022-04-27 12:23:06 -04:00
if ( priority ) { pointerText = html ` <div class="Badge Next">Next</div> ` ; }
if ( item . timeframe == "preroll" ) { pointerText = html ` <div class="Badge Soon">Soon</div> ` ; }
if ( item . timeframe == "current" ) { pointerText = html ` <div class="Badge Live">Live</div> ` ; }
2022-04-25 17:23:23 -04:00
2022-04-26 18:01:57 -04:00
// (date) partition
2022-04-25 17:23:23 -04:00
let partition ;
if ( ! previous || ( previous . start . Date !== item . start . Date ) )
{
2022-04-26 16:00:04 -04:00
partition = html ` <h3 class="Partition" key= ${ item . id + item . start . Day } >
$ { item . start . Day } , $ { item . start . Month } $ { item . start . Date }
< / h 3 > ` ;
2022-04-25 17:23:23 -04:00
}
2022-04-26 18:01:57 -04:00
// button
2022-04-25 17:23:23 -04:00
let buttonText ;
2022-04-27 12:23:06 -04:00
if ( item . timeframe == "past" ) { buttonText = "Rewatch" ; }
if ( item . timeframe == "current" || item . timeframe == "preroll" ) { buttonText = "Watch" ; }
if ( item . timeframe == "future" ) { buttonText = "Preview" ; }
2022-04-22 16:56:41 -04:00
return html `
2022-04-25 17:23:23 -04:00
$ { partition }
2022-04-27 08:25:22 -04:00
< div class = $ { ` Broadcast ${ item . timeframe } ` } key = $ { item . id } >
2022-04-26 13:49:43 -04:00
< div class = "Time" > $ { item . start . Hours } : $ { item . start . Minutes } $ { item . start . M } < / d i v >
< div class = "Title" > $ { item . name } < / s t r o n g >
< div class = "Control" >
< button onClick = $ { select } disabled = $ { selected } > $ { buttonText } < / b u t t o n >
< / d i v >
2022-04-27 12:23:06 -04:00
< div class = "Pointer" > $ { pointerText } < / d i v >
2022-04-25 17:23:23 -04:00
< / d i v > ` ;
2022-04-22 16:56:41 -04:00
} ;
/** @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 ( ) ,
2022-04-26 10:39:25 -04:00
Epoch : date . valueOf ( )
2022-04-22 16:56:41 -04:00
} ;
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 ;
} ;
2022-04-27 16:37:53 -04:00
/** @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 ) ;