2021-01-10 06:27:30 +00:00
|
|
|
|
import React, {useState, useRef, useLayoutEffect, useReducer} from 'react';
|
|
|
|
|
import './CreateLeague.css';
|
|
|
|
|
import twemoji from 'twemoji';
|
|
|
|
|
|
|
|
|
|
interface LeagueStructureState {
|
|
|
|
|
subleagues: SubleagueState[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SubleagueState {
|
|
|
|
|
name: string
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: string|number
|
2021-01-10 06:27:30 +00:00
|
|
|
|
divisions: DivisionState[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DivisionState {
|
|
|
|
|
name: string
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: string|number
|
2021-01-10 06:27:30 +00:00
|
|
|
|
teams: TeamState[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TeamState {
|
|
|
|
|
name: string
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: string|number
|
2021-01-10 06:27:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LeagueReducerActions =
|
|
|
|
|
{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}
|
|
|
|
|
|
|
|
|
|
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
|
|
|
|
|
2021-01-11 04:47:49 +00:00
|
|
|
|
let getUID = function() { // does NOT generate UUIDs. Meant to create list keys ONLY
|
|
|
|
|
let id = 0;
|
|
|
|
|
return function() { return id++}
|
|
|
|
|
}()
|
|
|
|
|
|
2021-01-10 06:27:30 +00:00
|
|
|
|
function leagueStructureReducer(state: LeagueStructureState, action: LeagueReducerActions): LeagueStructureState {
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
case 'remove_subleague':
|
|
|
|
|
return {subleagues: removeIndex(state.subleagues, action.subleague_index)};
|
|
|
|
|
case 'add_subleague':
|
|
|
|
|
return {subleagues: state.subleagues.concat([{
|
|
|
|
|
name: "",
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: getUID(),
|
2021-01-10 06:27:30 +00:00
|
|
|
|
divisions: arrayOf(state.subleagues[0].divisions.length, i => ({
|
|
|
|
|
name: "",
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: getUID(),
|
2021-01-10 06:27:30 +00:00
|
|
|
|
teams: []
|
|
|
|
|
}))
|
|
|
|
|
}])};
|
|
|
|
|
case 'rename_subleague':
|
|
|
|
|
return replaceSubleague(state, action.subleague_index, subleague => ({
|
|
|
|
|
name: action.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: subleague.id,
|
2021-01-10 06:27:30 +00:00
|
|
|
|
divisions: subleague.divisions
|
|
|
|
|
}));
|
|
|
|
|
case 'remove_divisions':
|
|
|
|
|
return {subleagues: state.subleagues.map(subleague => ({
|
|
|
|
|
name: subleague.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: subleague.id,
|
2021-01-10 06:27:30 +00:00
|
|
|
|
divisions: removeIndex(subleague.divisions, action.division_index)
|
|
|
|
|
}))};
|
|
|
|
|
case 'add_divisions':
|
|
|
|
|
return {subleagues: state.subleagues.map(subleague => ({
|
|
|
|
|
name: subleague.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: subleague.id,
|
|
|
|
|
divisions: subleague.divisions.concat([{
|
|
|
|
|
name: "",
|
|
|
|
|
id: getUID(),
|
|
|
|
|
teams: []
|
|
|
|
|
}])
|
2021-01-10 06:27:30 +00:00
|
|
|
|
}))};
|
|
|
|
|
case 'rename_division':
|
|
|
|
|
return replaceDivision(state, action.subleague_index, action.division_index, division => ({
|
|
|
|
|
name: action.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: division.id,
|
2021-01-10 06:27:30 +00:00
|
|
|
|
teams: division.teams
|
|
|
|
|
}));
|
|
|
|
|
case 'remove_team':
|
|
|
|
|
return replaceDivision(state, action.subleague_index, action.division_index, division => ({
|
|
|
|
|
name: division.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: division.id,
|
2021-01-10 06:27:30 +00:00
|
|
|
|
teams: removeIndex(division.teams, division.teams.findIndex(val => val.name === action.name))
|
|
|
|
|
}));
|
|
|
|
|
case 'add_team':
|
|
|
|
|
return replaceDivision(state, action.subleague_index, action.division_index, division => ({
|
|
|
|
|
name: division.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: division.id,
|
|
|
|
|
teams: division.teams.concat([{
|
|
|
|
|
name: action.name,
|
|
|
|
|
id: getUID()
|
|
|
|
|
}])
|
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) {
|
|
|
|
|
return replaceSubleague(state, si, subleague => ({
|
|
|
|
|
name: subleague.name,
|
2021-01-11 04:47:49 +00:00
|
|
|
|
id: subleague.id,
|
2021-01-10 06:27:30 +00:00
|
|
|
|
divisions: replaceIndex(subleague.divisions, di, func(subleague.divisions[di]))
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-11 04:47:49 +00:00
|
|
|
|
let initLeagueStructure = {
|
|
|
|
|
subleagues: [0, 1].map((val) => ({
|
|
|
|
|
name: "",
|
|
|
|
|
id: getUID(),
|
|
|
|
|
divisions: [0, 1].map((val) => ({
|
|
|
|
|
name: "",
|
|
|
|
|
id: getUID(),
|
|
|
|
|
teams: []
|
|
|
|
|
}))
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-10 06:27:30 +00:00
|
|
|
|
function CreateLeague() {
|
|
|
|
|
let [name, setName] = useState("");
|
|
|
|
|
let [structure, dispatch] = useReducer(leagueStructureReducer, initLeagueStructure);
|
|
|
|
|
|
|
|
|
|
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)}/>
|
|
|
|
|
<LeagueStructre state={structure} dispatch={dispatch}/>
|
|
|
|
|
<LeagueOptions />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch<LeagueReducerActions>}) {
|
|
|
|
|
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-10 06:27:30 +00:00
|
|
|
|
<SubleagueHeaders subleagues={props.state.subleagues} dispatch={props.dispatch} />
|
|
|
|
|
<Divisions subleagues={props.state.subleagues} dispatch={props.dispatch} />
|
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>
|
|
|
|
|
<button className="cl_division_add" onClick={e => props.dispatch({type: 'add_divisions'})}>➕</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<LeagueReducerActions>}) {
|
|
|
|
|
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))
|
|
|
|
|
}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2021-01-10 06:27:30 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit<LeagueReducerActions, 'subleague_index'>) => void}) {
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<LeagueReducerActions>}) {
|
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))
|
|
|
|
|
}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</>);
|
2021-01-10 06:27:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit<LeagueReducerActions, 'subleague_index'|'division_index'>) => void}) {
|
|
|
|
|
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-11 21:11:35 +00:00
|
|
|
|
<div>
|
|
|
|
|
<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})
|
|
|
|
|
}/>
|
|
|
|
|
</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-10 06:27:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LeagueOptions() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="cl_league_options">
|
2021-01-11 04:47:49 +00:00
|
|
|
|
|
2021-01-10 06:27:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CreateLeague;
|