sim16/simmadome/src/CreateLeague.tsx

464 lines
16 KiB
TypeScript
Raw Normal View History

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-12 18:16:31 +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-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)
}
})
return (
<div className="cl_league_main" ref={self}>
<input type="text" className="cl_league_name" placeholder="League Name" value={name} onChange={(e) => setName(e.target.value)}/>
2021-01-12 08:17:02 +00:00
<div className="cl_structure_err">{name === "" && showError ? "A name is required." : ""}</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 => {
//make network call, once leagues are merged
2021-01-12 18:16:31 +00:00
if (!validRequest(name, structure, options)) {
2021-01-12 08:17:02 +00:00
setShowError(true);
}
}}>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-12 08:17:02 +00:00
2021-01-12 18:16:31 +00:00
if (!validRequest(name, structure, options)) {
2021-01-12 08:17:02 +00:00
return null
}
return ({
structure: {
name: name,
subleagues: structure.subleagues.map(subleague => ({
name: subleague.name,
divisions: subleague.divisions.map(division => ({
name: division.name,
teams: division.teams
}))
}))
},
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) &&
validNumber(options.wildcards) &&
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
)
)
)
}
function validNumber(value: string) {
return Number(value) !== NaN && Number(value) > 0
}
// 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
type StateBundle<T> = [T, React.Dispatch<React.SetStateAction<T>>]
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}/>
<NumberInput title="Number of wildcards" value={props.state.wildcards} setValue={(value: string) =>
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-12 08:17:02 +00:00
function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean}) {
return (
<div className="cl_option_box">
<div className="cl_option_label">{props.title}</div>
<input className="cl_option_input" type="number" min="0" value={props.value} onChange={e => props.setValue(e.target.value)}/>
<div className="cl_option_err">{(Number(props.value) === NaN || Number(props.value) < 0) && props.showError ? "Must be a number greater than 0" : ""}</div>
2021-01-10 06:27:30 +00:00
</div>
);
}
export default CreateLeague;