2021-01-10 06:27:30 +00:00
import React , { useState , useRef , useLayoutEffect , useReducer } from 'react' ;
import './CreateLeague.css' ;
import twemoji from 'twemoji' ;
2021-01-12 08:17:02 +00:00
// STATE CLASSES
class LeagueStructureState {
2021-01-10 06:27:30 +00:00
subleagues : SubleagueState [ ]
2021-01-12 08:17:02 +00:00
constructor ( subleagues : SubleagueState [ ] = [ ] ) {
this . subleagues = subleagues ;
}
2021-01-10 06:27:30 +00:00
}
2021-01-12 08:17:02 +00:00
class SubleagueState {
2021-01-10 06:27:30 +00:00
name : string
divisions : DivisionState [ ]
2021-01-12 08:17:02 +00:00
id : string | number
constructor ( divisions : DivisionState [ ] = [ ] ) {
this . name = "" ;
this . divisions = divisions ;
this . id = getUID ( ) ;
}
2021-01-10 06:27:30 +00:00
}
2021-01-12 08:17:02 +00:00
class DivisionState {
2021-01-10 06:27:30 +00:00
name : string
teams : TeamState [ ]
2021-01-12 08:17:02 +00:00
id : string | number
constructor ( ) {
this . name = "" ;
this . teams = [ ] ;
this . id = getUID ( ) ;
}
2021-01-10 06:27:30 +00:00
}
2021-01-12 08:17:02 +00:00
class TeamState {
2021-01-10 06:27:30 +00:00
name : string
2021-01-11 04:47:49 +00:00
id : string | number
2021-01-12 08:17:02 +00:00
constructor ( name : string = "" ) {
this . name = name ;
this . id = getUID ( ) ;
}
2021-01-10 06:27:30 +00:00
}
2021-01-12 08:17:02 +00:00
let getUID = function ( ) { // does NOT generate UUIDs. Meant to create list keys ONLY
let id = 0 ;
return function ( ) { return id ++ }
} ( )
// STRUCTURE REDUCER
type StructureReducerActions =
2021-01-10 06:27:30 +00:00
{ type : 'remove_subleague' , subleague_index : number } |
{ type : 'add_subleague' } |
{ type : 'rename_subleague' , subleague_index : number , name : string } |
{ type : 'remove_divisions' , division_index : number } |
{ type : 'add_divisions' } |
{ type : 'rename_division' , subleague_index : number , division_index : number , name : string } |
{ type : 'remove_team' , subleague_index : number , division_index : number , name :string } |
{ type : 'add_team' , subleague_index :number , division_index :number , name :string }
2021-01-12 08:17:02 +00:00
function leagueStructureReducer ( state : LeagueStructureState , action : StructureReducerActions ) : LeagueStructureState {
2021-01-10 06:27:30 +00:00
switch ( action . type ) {
case 'remove_subleague' :
return { subleagues : removeIndex ( state . subleagues , action . subleague_index ) } ;
case 'add_subleague' :
2021-01-12 08:17:02 +00:00
return { subleagues : append ( state . subleagues , new SubleagueState (
arrayOf ( state . subleagues [ 0 ] . divisions . length , i = >
new DivisionState ( )
)
) ) }
2021-01-10 06:27:30 +00:00
case 'rename_subleague' :
2021-01-12 08:17:02 +00:00
return replaceSubleague ( state , action . subleague_index , subleague = > {
let nSubleague = shallowClone ( subleague ) ;
nSubleague . name = action . name ;
return nSubleague ;
} ) ;
2021-01-10 06:27:30 +00:00
case 'remove_divisions' :
2021-01-12 08:17:02 +00:00
return { subleagues : state.subleagues.map ( subleague = > {
let nSubleague = shallowClone ( subleague ) ;
nSubleague . divisions = removeIndex ( subleague . divisions , action . division_index )
return nSubleague ;
} ) } ;
2021-01-10 06:27:30 +00:00
case 'add_divisions' :
2021-01-12 08:17:02 +00:00
return { subleagues : state.subleagues.map ( subleague = > {
let nSubleague = shallowClone ( subleague ) ;
nSubleague . divisions = append ( subleague . divisions , new DivisionState ( ) )
return nSubleague ;
} ) } ;
2021-01-10 06:27:30 +00:00
case 'rename_division' :
2021-01-12 08:17:02 +00:00
return replaceDivision ( state , action . subleague_index , action . division_index , division = > {
let nDivision = shallowClone ( division ) ;
nDivision . name = action . name ;
return nDivision ;
} ) ;
2021-01-10 06:27:30 +00:00
case 'remove_team' :
2021-01-12 08:17:02 +00:00
return replaceDivision ( state , action . subleague_index , action . division_index , division = > {
let nDivision = shallowClone ( division ) ;
nDivision . teams = removeIndex ( division . teams , division . teams . findIndex ( val = > val . name === action . name ) ) ;
return nDivision ;
} ) ;
2021-01-10 06:27:30 +00:00
case 'add_team' :
2021-01-12 08:17:02 +00:00
return replaceDivision ( state , action . subleague_index , action . division_index , division = > {
let nDivision = shallowClone ( division ) ;
nDivision . teams = append ( division . teams , new TeamState ( action . name ) ) ;
return nDivision ;
} ) ;
2021-01-10 06:27:30 +00:00
}
}
function replaceSubleague ( state : LeagueStructureState , si : number , func : ( val : SubleagueState ) = > SubleagueState ) {
return { subleagues : replaceIndex ( state . subleagues , si , func ( state . subleagues [ si ] ) ) }
}
function replaceDivision ( state : LeagueStructureState , si : number , di : number , func : ( val : DivisionState ) = > DivisionState ) {
2021-01-12 08:17:02 +00:00
return replaceSubleague ( state , si , subleague = > {
let nSubleague = shallowClone ( subleague ) ;
nSubleague . divisions = replaceIndex ( subleague . divisions , di , func ( subleague . divisions [ di ] ) ) ;
return nSubleague ;
} ) ;
2021-01-10 06:27:30 +00:00
}
2021-01-12 18:16:31 +00:00
// OPTIONS REDUCER
class LeagueOptionsState {
games_series = "3"
intra_division_series = "8"
inter_division_series = "16"
inter_league_series = "8"
top_postseason = "1"
wildcards = "0"
}
type OptionsReducerActions =
{ type : 'set_games_series' , value : string } |
{ type : 'set_intra_division_series' , value : string } |
{ type : 'set_inter_division_series' , value : string } |
{ type : 'set_inter_league_series' , value : string } |
{ type : 'set_top_postseason' , value : string } |
{ type : 'set_wildcards' , value : string }
function LeagueOptionsReducer ( state : LeagueOptionsState , action : OptionsReducerActions ) {
let newState = shallowClone ( state ) ;
switch ( action . type ) {
case 'set_games_series' :
newState . games_series = action . value ;
break ;
case 'set_intra_division_series' :
newState . intra_division_series = action . value ;
break ;
case 'set_inter_division_series' :
newState . inter_division_series = action . value ;
break ;
case 'set_inter_league_series' :
newState . inter_league_series = action . value ;
break ;
case 'set_top_postseason' :
newState . top_postseason = action . value ;
break ;
case 'set_wildcards' :
newState . wildcards = action . value ;
break ;
}
return newState
}
2021-01-12 08:17:02 +00:00
// UTIL
2021-01-10 06:27:30 +00:00
function removeIndex ( arr : any [ ] , index : number ) {
return arr . slice ( 0 , index ) . concat ( arr . slice ( index + 1 ) ) ;
}
function replaceIndex < T > ( arr : T [ ] , index : number , val : T ) {
return arr . slice ( 0 , index ) . concat ( [ val ] ) . concat ( arr . slice ( index + 1 ) ) ;
}
function append < T > ( arr : T [ ] , val : T ) {
return arr . concat ( [ val ] ) ;
}
function arrayOf < T > ( length : number , func : ( i : number ) = > T ) : T [ ] {
var out : T [ ] = [ ] ;
for ( var i = 0 ; i < length ; i ++ ) {
out . push ( func ( i ) ) ;
}
return out ;
}
2021-01-12 08:17:02 +00:00
function shallowClone < T > ( obj : T ) : T {
return Object . assign ( { } , obj ) ;
2021-01-11 04:47:49 +00:00
}
2021-01-12 08:17:02 +00:00
type DistributiveOmit < T , K extends keyof any > = T extends any ? Omit < T , K > : never ;
2021-01-14 22:58:24 +00:00
//type DistributivePick<T, K extends keyof T> = T extends any ? Pick<T, K> : never;
2021-01-12 08:17:02 +00:00
// CREATE LEAGUE
2021-01-12 18:16:31 +00:00
let initLeagueStructure = {
subleagues : [ 0 , 1 ] . map ( ( val ) = >
new SubleagueState ( [ 0 , 1 ] . map ( ( val ) = >
new DivisionState ( )
) )
)
} ;
2021-01-10 06:27:30 +00:00
function CreateLeague() {
let [ name , setName ] = useState ( "" ) ;
2021-01-12 08:17:02 +00:00
let [ showError , setShowError ] = useState ( false ) ;
2021-01-14 06:30:31 +00:00
let [ nameExists , setNameExists ] = useState ( false ) ;
let [ createSuccess , setCreateSuccess ] = useState ( false ) ;
2021-01-12 18:16:31 +00:00
let [ structure , structureDispatch ] = useReducer ( leagueStructureReducer , initLeagueStructure ) ;
let [ options , optionsDispatch ] = useReducer ( LeagueOptionsReducer , new LeagueOptionsState ( ) ) ;
2021-01-10 06:27:30 +00:00
let self = useRef < HTMLDivElement | null > ( null )
useLayoutEffect ( ( ) = > {
if ( self . current ) {
twemoji . parse ( self . current )
}
} )
2021-01-14 06:30:31 +00:00
if ( createSuccess ) {
return (
< div className = "cl_league_main" ref = { self } >
< div className = "cl_confirm_box" >
League created succesfully !
< / div >
< / div >
) ;
}
2021-01-10 06:27:30 +00:00
return (
< div className = "cl_league_main" ref = { self } >
2021-01-14 06:30:31 +00:00
< input type = "text" className = "cl_league_name" placeholder = "League Name" value = { name } onChange = { ( e ) = > {
setName ( e . target . value ) ;
setNameExists ( false ) ;
} } / >
< div className = "cl_structure_err" > {
name === "" && showError ? "A name is required." :
nameExists && showError ? "A league by that name already exists" :
""
} < / div >
2021-01-12 18:16:31 +00:00
< LeagueStructre state = { structure } dispatch = { structureDispatch } showError = { showError } / >
2021-01-12 08:17:02 +00:00
< div className = "cl_league_options" >
2021-01-12 18:16:31 +00:00
< LeagueOptions state = { options } dispatch = { optionsDispatch } showError = { showError } / >
2021-01-12 08:17:02 +00:00
< div className = "cl_option_submit_box" >
< button className = "cl_option_submit" onClick = { e = > {
2021-01-12 18:16:31 +00:00
if ( ! validRequest ( name , structure , options ) ) {
2021-01-12 08:17:02 +00:00
setShowError ( true ) ;
2021-01-14 06:30:31 +00:00
} else {
let req = new XMLHttpRequest ( ) ;
let data = makeRequest ( name , structure , options ) ;
req . open ( "POST" , "/api/leagues" , true ) ;
req . setRequestHeader ( "Content-type" , "application/json" ) ;
req . onreadystatechange = ( ) = > {
if ( req . readyState === 4 ) {
if ( req . status === 200 ) {
setCreateSuccess ( true ) ;
}
if ( req . status === 400 ) {
setNameExists ( true ) ;
setShowError ( true ) ;
}
}
}
req . send ( data ) ;
2021-01-12 08:17:02 +00:00
}
} } > Submit < / button >
< div className = "cl_option_err" > {
2021-01-12 18:16:31 +00:00
! validRequest ( name , structure , options ) && showError ?
"Cannot create league. Some information is missing or invalid." : ""
2021-01-12 08:17:02 +00:00
} < / div >
< / div >
< / div >
2021-01-10 06:27:30 +00:00
< / div >
) ;
}
2021-01-12 18:16:31 +00:00
function makeRequest ( name :string , structure : LeagueStructureState , options :LeagueOptionsState ) {
2021-01-14 06:30:31 +00:00
return JSON . stringify ( {
name : name ,
2021-01-12 08:17:02 +00:00
structure : {
subleagues : structure.subleagues.map ( subleague = > ( {
name : subleague.name ,
divisions : subleague.divisions.map ( division = > ( {
name : division.name ,
2021-01-14 06:30:31 +00:00
teams : division.teams.map ( team = > team . name )
2021-01-12 08:17:02 +00:00
} ) )
} ) )
} ,
2021-01-12 18:16:31 +00:00
games_per_series : Number ( options . games_series ) ,
division_series : Number ( options . intra_division_series ) ,
inter_division_series : Number ( options . inter_division_series ) ,
inter_league_series : Number ( options . inter_league_series ) ,
top_postseason : Number ( options . top_postseason ) ,
wildcards : Number ( options . wildcards )
2021-01-12 08:17:02 +00:00
} ) ;
}
2021-01-12 18:16:31 +00:00
function validRequest ( name :string , structure : LeagueStructureState , options :LeagueOptionsState ) {
2021-01-12 08:17:02 +00:00
return (
name !== "" &&
2021-01-12 18:16:31 +00:00
validNumber ( options . games_series ) &&
validNumber ( options . intra_division_series ) &&
validNumber ( options . inter_division_series ) &&
validNumber ( options . inter_league_series ) &&
validNumber ( options . top_postseason ) &&
2021-01-14 06:30:31 +00:00
validNumber ( options . wildcards , 0 ) &&
2021-01-12 08:17:02 +00:00
structure . subleagues . length % 2 === 0 &&
structure . subleagues . every ( subleague = >
subleague . name !== "" &&
subleague . divisions . every ( division = >
division . name !== "" &&
division . teams . length >= 2
)
)
)
}
2021-01-14 06:30:31 +00:00
function validNumber ( value : string , min = 1 ) {
2021-01-14 22:58:24 +00:00
return ! isNaN ( Number ( value ) ) && Number ( value ) >= min
2021-01-12 08:17:02 +00:00
}
// LEAGUE STRUCUTRE
function LeagueStructre ( props : { state : LeagueStructureState , dispatch : React.Dispatch < StructureReducerActions > , showError : boolean } ) {
2021-01-10 06:27:30 +00:00
return (
< div className = "cl_league_structure" >
< div className = "cl_league_structure_scrollbox" >
< div className = "cl_subleague_add_align" >
2021-01-11 04:47:49 +00:00
< div className = "cl_league_structure_table" >
2021-01-12 08:17:02 +00:00
< SubleagueHeaders subleagues = { props . state . subleagues } dispatch = { props . dispatch } showError = { props . showError } / >
< Divisions subleagues = { props . state . subleagues } dispatch = { props . dispatch } showError = { props . showError } / >
2021-01-11 04:47:49 +00:00
< / div >
2021-01-10 06:27:30 +00:00
< button className = "cl_subleague_add" onClick = { e = > props . dispatch ( { type : 'add_subleague' } ) } > ➕ < / button >
< / div >
< / div >
2021-01-12 08:17:02 +00:00
< div className = "cl_structure_err" > { props . state . subleagues . length % 2 !== 0 && props . showError ? "Must have an even number of subleagues." : "" } < / div >
2021-01-10 06:27:30 +00:00
< button className = "cl_division_add" onClick = { e = > props . dispatch ( { type : 'add_divisions' } ) } > ➕ < / button >
< / div >
) ;
}
2021-01-12 08:17:02 +00:00
function SubleagueHeaders ( props : { subleagues : SubleagueState [ ] , dispatch : React.Dispatch < StructureReducerActions > , showError :boolean } ) {
2021-01-10 06:27:30 +00:00
return (
2021-01-11 04:47:49 +00:00
< div className = "cl_headers" >
< div key = "filler" className = "cl_delete_filler" / >
{ props . subleagues . map ( ( subleague , i ) = > (
< div key = { subleague . id } className = "cl_table_header" >
< div className = "cl_subleague_bg" >
< SubleageHeader state = { subleague } canDelete = { props . subleagues . length > 1 } dispatch = { action = >
props . dispatch ( Object . assign ( { subleague_index : i } , action ) )
} / >
2021-01-12 08:17:02 +00:00
< div className = "cl_structure_err" > { subleague . name === "" && props . showError ? "A name is required." : "" } < / div >
2021-01-11 04:47:49 +00:00
< / div >
< / div >
) ) }
< / div >
2021-01-10 06:27:30 +00:00
) ;
}
2021-01-12 08:17:02 +00:00
function SubleageHeader ( props : { state : SubleagueState , canDelete : boolean , dispatch : ( action : DistributiveOmit < StructureReducerActions , ' subleague_index ' > ) = > void } ) {
2021-01-10 06:27:30 +00:00
return (
< div className = "cl_subleague_header" >
< input type = "text" className = "cl_subleague_name" placeholder = "Subleague Name" value = { props . state . name } onChange = { e = >
props . dispatch ( { type : 'rename_subleague' , name : e.target.value } )
} / >
{ props . canDelete ? < button className = "cl_subleague_delete" onClick = { e = > props . dispatch ( { type : 'remove_subleague' } ) } > ➖ < / button > : null }
< / div >
) ;
}
2021-01-12 08:17:02 +00:00
function Divisions ( props : { subleagues : SubleagueState [ ] , dispatch : React.Dispatch < StructureReducerActions > , showError : boolean } ) {
2021-01-11 04:47:49 +00:00
return ( < >
{ props . subleagues [ 0 ] . divisions . map ( ( val , di ) = > (
< div key = { val . id } className = "cl_table_row" >
< div key = "delete" className = "cl_delete_box" >
{ props . subleagues [ 0 ] . divisions . length > 1 ?
< button className = "cl_division_delete" onClick = { e = > props . dispatch ( { type : 'remove_divisions' , division_index : di } ) } > ➖ < / button > :
null
}
< / div >
{ props . subleagues . map ( ( subleague , si ) = > (
< div key = { subleague . id } className = "cl_division_cell" >
< div className = "cl_subleague_bg" >
< Division state = { subleague . divisions [ di ] } dispatch = { action = >
props . dispatch ( Object . assign ( { subleague_index : si , division_index : di } , action ) )
2021-01-12 08:17:02 +00:00
} showError = { props . showError } / >
2021-01-11 04:47:49 +00:00
< / div >
< / div >
) ) }
< / div >
) ) }
< / > ) ;
2021-01-10 06:27:30 +00:00
}
2021-01-12 08:17:02 +00:00
function Division ( props : { state : DivisionState , dispatch : ( action : DistributiveOmit < StructureReducerActions , ' subleague_index ' | ' division_index ' > ) = > void , showError :boolean } ) {
2021-01-10 06:27:30 +00:00
let [ newName , setNewName ] = useState ( "" ) ;
2021-01-11 04:47:49 +00:00
let [ searchResults , setSearchResults ] = useState < string [ ] > ( [ ] ) ;
let newNameInput = useRef < HTMLInputElement > ( null ) ;
let resultList = useRef < HTMLDivElement > ( null ) ;
useLayoutEffect ( ( ) = > {
if ( resultList . current ) {
twemoji . parse ( resultList . current )
}
} )
2021-01-10 06:27:30 +00:00
return (
< div className = "cl_division" >
2021-01-12 08:17:02 +00:00
< div className = "cl_division_name_box" >
2021-01-11 21:11:35 +00:00
< input type = "text" className = "cl_division_name" placeholder = "Division Name" key = "input" value = { props . state . name } onChange = { e = >
props . dispatch ( { type : 'rename_division' , name : e.target.value } )
} / >
2021-01-12 08:17:02 +00:00
< div className = "cl_structure_err cl_structure_err_div" > { props . state . name === "" && props . showError ? "A name is required." : "" } < / div >
2021-01-11 21:11:35 +00:00
< / div >
2021-01-10 06:27:30 +00:00
{ props . state . teams . map ( ( team , i ) = > (
2021-01-11 04:47:49 +00:00
< div className = "cl_team" key = { team . id } >
2021-01-10 06:27:30 +00:00
< div className = "cl_team_name" > { team . name } < / div >
< button className = "cl_team_delete" onClick = { e = > props . dispatch ( { type : 'remove_team' , name : team.name } ) } > ➖ < / button >
< / div >
) ) }
< div className = "cl_team_add" >
2021-01-11 04:47:49 +00:00
< input type = "text" className = "cl_newteam_name" placeholder = "Add team..." value = { newName } ref = { newNameInput }
onChange = { e = > {
let params = new URLSearchParams ( { query : e.target.value , page_len : '5' , page_num : '0' } ) ;
2021-01-11 19:14:54 +00:00
fetch ( "/api/teams/search?" + params . toString ( ) )
. then ( response = > response . json ( ) )
. then ( data = > setSearchResults ( data ) ) ;
2021-01-11 04:47:49 +00:00
setNewName ( e . target . value ) ;
} } / >
2021-01-10 06:27:30 +00:00
< / div >
2021-01-11 04:47:49 +00:00
{ searchResults . length > 0 && newName . length > 0 ?
( < div className = "cl_search_list" ref = { resultList } >
{ searchResults . map ( result = >
< div className = "cl_search_result" key = { result } onClick = { e = > {
props . dispatch ( { type : 'add_team' , name : result } ) ;
setNewName ( "" ) ;
if ( newNameInput . current ) {
newNameInput . current . focus ( ) ;
}
} } > { result } < / div >
) }
< / div > ) :
< div / >
}
2021-01-12 08:17:02 +00:00
< div className = "cl_structure_err cl_structure_err_teams" > { props . state . teams . length < 2 && props . showError ? "Must have at least 2 teams." : "" } < / div >
2021-01-10 06:27:30 +00:00
< / div >
) ;
}
2021-01-12 08:17:02 +00:00
// LEAGUE OPTIONS
2021-01-12 18:16:31 +00:00
function LeagueOptions ( props : { state : LeagueOptionsState , dispatch : React.Dispatch < OptionsReducerActions > , showError : boolean } ) {
2021-01-10 06:27:30 +00:00
return (
2021-01-12 08:17:02 +00:00
< div className = "cl_option_main" >
< div className = "cl_option_column" >
2021-01-12 18:16:31 +00:00
< NumberInput title = "Number of games per series" value = { props . state . games_series } setValue = { ( value : string ) = >
props . dispatch ( { type : 'set_games_series' , value : value } ) } showError = { props . showError } / >
< NumberInput title = "Number of teams from top of division to postseason" value = { props . state . top_postseason } setValue = { ( value : string ) = >
props . dispatch ( { type : 'set_top_postseason' , value : value } ) } showError = { props . showError } / >
2021-01-14 22:58:24 +00:00
< NumberInput title = "Number of wildcards" value = { props . state . wildcards } minValue = { 0 } setValue = { ( value : string ) = >
2021-01-12 18:16:31 +00:00
props . dispatch ( { type : 'set_wildcards' , value : value } ) } showError = { props . showError } / >
2021-01-12 08:17:02 +00:00
< / div >
< div className = "cl_option_column" >
2021-01-12 18:16:31 +00:00
< NumberInput title = "Number of series with each division opponent" value = { props . state . intra_division_series } setValue = { ( value : string ) = >
props . dispatch ( { type : 'set_intra_division_series' , value : value } ) } showError = { props . showError } / >
< NumberInput title = "Number of inter-divisional series" value = { props . state . inter_division_series } setValue = { ( value : string ) = >
props . dispatch ( { type : 'set_inter_division_series' , value : value } ) } showError = { props . showError } / >
< NumberInput title = "Number of inter-league series" value = { props . state . inter_league_series } setValue = { ( value : string ) = >
props . dispatch ( { type : 'set_inter_league_series' , value : value } ) } showError = { props . showError } / >
2021-01-12 08:17:02 +00:00
< / div >
< / div >
) ;
}
2021-01-11 04:47:49 +00:00
2021-01-14 22:58:24 +00:00
function NumberInput ( props : { title : string , value : string , setValue : ( newVal : string ) = > void , showError : boolean , minValue? :number } ) {
let minValue = 1 ;
if ( props . minValue !== undefined ) {
minValue = props . minValue
}
2021-01-12 08:17:02 +00:00
return (
< div className = "cl_option_box" >
< div className = "cl_option_label" > { props . title } < / div >
2021-01-14 22:58:24 +00:00
< input className = "cl_option_input" type = "number" min = { minValue } value = { props . value } onChange = { e = > props . setValue ( e . target . value ) } / >
< div className = "cl_option_err" > { ( ! isNaN ( Number ( props . value ) ) || Number ( props . value ) < minValue ) && props . showError ? "Must be a number greater than " + minValue : "" } < / div >
2021-01-10 06:27:30 +00:00
< / div >
) ;
}
export default CreateLeague ;