diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css index 5d6971b..58c3e36 100644 --- a/simmadome/src/CreateLeague.css +++ b/simmadome/src/CreateLeague.css @@ -27,6 +27,16 @@ input:focus { outline: none; } +input[type="number"] { + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; +} + .cl_league_main { display: flex; flex-direction: column; @@ -36,7 +46,7 @@ input:focus { } .cl_league_name { - margin: 1rem; + margin-top: 1rem; } .cl_league_options, .cl_league_structure { @@ -48,6 +58,10 @@ input:focus { padding-top: 1.5rem; } +.cl_league_options { + align-items: center; +} + .cl_league_structure, .cl_subleague_add_align { display: flex; align-items: center; @@ -55,6 +69,10 @@ input:focus { width: min-content; } +.cl_league_structure { + margin-top: 1rem; +} + .cl_league_structure_table { display: table; margin-right: 1rem; @@ -106,8 +124,8 @@ input:focus { background: var(--background-main); padding: 0.5rem 1rem; margin: 0rem 0.5rem; - width: 22rem; - height:100%; + min-width: 22rem; + height: 100%; box-sizing: border-box; } @@ -116,6 +134,10 @@ input:focus { margin-right: 0.5rem; } +.cl_division_name_box { + width: 100%; +} + .cl_division_name { margin-bottom: 0.5rem; width: 95%; @@ -145,7 +167,7 @@ input:focus { .cl_team_name { font-size: 14pt; - padding-right: 0.5rem; + padding: 0 0.5rem; overflow: hidden; text-overflow: ellipsis; } @@ -173,6 +195,58 @@ input:focus { background: var(--background-main); } +.cl_league_options { + padding: 1rem; + margin-top: 1.5rem; + width: 55rem; + box-sizing: border-box; +} + +.cl_option_main { + display: flex; + flex-direction: row; + justify-content: space-around; + width: 100%; +} + +.cl_option_submit_box { + display: flex; + flex-direction: column; + align-items: center; +} + +.cl_option_box { + margin: 1rem; + width: max-content; +} + +.cl_option_label, .cl_option_err, .cl_structure_err { + margin: 0.25rem; +} + +.cl_option_err, .cl_structure_err { + color: var(--accent-red); +} + +.cl_option_err { + min-height: 1.5rem; + margin-bottom: -0.5rem; + margin-top: 0.5rem; +} + +.cl_structure_err { + margin-bottom: -0.5rem; +} + +.cl_structure_err_div { + margin-top: -0.5rem; + margin-bottom: 0; +} + +.cl_structure_err_teams { + width: 98%; +} + /* button styles */ button > .emoji { @@ -212,4 +286,16 @@ button > .emoji { .cl_delete_filler { min-width: 3rem; +} + +.cl_option_submit { + padding: 1rem 2rem; + height: 2rem; + border: none; + border-radius: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-green); + font-size: 14pt; } \ No newline at end of file diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index 5751f78..f0e4df6 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -2,28 +2,66 @@ import React, {useState, useRef, useLayoutEffect, useReducer} from 'react'; import './CreateLeague.css'; import twemoji from 'twemoji'; -interface LeagueStructureState { +// STATE CLASSES + +class LeagueStructureState { subleagues: SubleagueState[] + + constructor(subleagues: SubleagueState[] = []) { + this.subleagues = subleagues; + } } -interface SubleagueState { +class SubleagueState { name: string - id: string|number divisions: DivisionState[] + id: string|number + + constructor(divisions: DivisionState[] = []) { + this.name = ""; + this.divisions = divisions; + this.id = getUID(); + } } -interface DivisionState { +class DivisionState { name: string - id: string|number teams: TeamState[] + id: string|number + + constructor() { + this.name = ""; + this.teams = []; + this.id = getUID(); + } } -interface TeamState { +class TeamState { name: string id: string|number + + constructor(name: string = "") { + this.name = name; + this.id = getUID(); + } } -type LeagueReducerActions = +let getUID = function() { // does NOT generate UUIDs. Meant to create list keys ONLY + let id = 0; + return function() { return id++ } +}() + +let initLeagueStructure = { + subleagues: [0, 1].map((val) => + new SubleagueState([0, 1].map((val) => + new DivisionState() + )) + ) +}; + +// STRUCTURE REDUCER + +type StructureReducerActions = {type: 'remove_subleague', subleague_index: number} | {type: 'add_subleague'} | {type: 'rename_subleague', subleague_index: number, name: string} | @@ -33,70 +71,52 @@ type LeagueReducerActions = {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 extends any ? Omit : never; - -let getUID = function() { // does NOT generate UUIDs. Meant to create list keys ONLY - let id = 0; - return function() { return id++} -}() - -function leagueStructureReducer(state: LeagueStructureState, action: LeagueReducerActions): LeagueStructureState { +function leagueStructureReducer(state: LeagueStructureState, action: StructureReducerActions): LeagueStructureState { switch (action.type) { case 'remove_subleague': return {subleagues: removeIndex(state.subleagues, action.subleague_index)}; case 'add_subleague': - return {subleagues: state.subleagues.concat([{ - name: "", - id: getUID(), - divisions: arrayOf(state.subleagues[0].divisions.length, i => ({ - name: "", - id: getUID(), - teams: [] - })) - }])}; + return {subleagues: append(state.subleagues, new SubleagueState( + arrayOf(state.subleagues[0].divisions.length, i => + new DivisionState() + ) + ))} case 'rename_subleague': - return replaceSubleague(state, action.subleague_index, subleague => ({ - name: action.name, - id: subleague.id, - divisions: subleague.divisions - })); + return replaceSubleague(state, action.subleague_index, subleague => { + let nSubleague = shallowClone(subleague); + nSubleague.name = action.name; + return nSubleague; + }); case 'remove_divisions': - return {subleagues: state.subleagues.map(subleague => ({ - name: subleague.name, - id: subleague.id, - divisions: removeIndex(subleague.divisions, action.division_index) - }))}; + return {subleagues: state.subleagues.map(subleague => { + let nSubleague = shallowClone(subleague); + nSubleague.divisions = removeIndex(subleague.divisions, action.division_index) + return nSubleague; + })}; case 'add_divisions': - return {subleagues: state.subleagues.map(subleague => ({ - name: subleague.name, - id: subleague.id, - divisions: subleague.divisions.concat([{ - name: "", - id: getUID(), - teams: [] - }]) - }))}; + return {subleagues: state.subleagues.map(subleague => { + let nSubleague = shallowClone(subleague); + nSubleague.divisions = append(subleague.divisions, new DivisionState()) + return nSubleague; + })}; case 'rename_division': - return replaceDivision(state, action.subleague_index, action.division_index, division => ({ - name: action.name, - id: division.id, - teams: division.teams - })); + return replaceDivision(state, action.subleague_index, action.division_index, division => { + let nDivision = shallowClone(division); + nDivision.name = action.name; + return nDivision; + }); case 'remove_team': - return replaceDivision(state, action.subleague_index, action.division_index, division => ({ - name: division.name, - id: division.id, - teams: removeIndex(division.teams, division.teams.findIndex(val => val.name === action.name)) - })); + 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; + }); case 'add_team': - return replaceDivision(state, action.subleague_index, action.division_index, division => ({ - name: division.name, - id: division.id, - teams: division.teams.concat([{ - name: action.name, - id: getUID() - }]) - })); + 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; + }); } } @@ -105,13 +125,15 @@ function replaceSubleague(state: LeagueStructureState, si: number, func: (val: S } function replaceDivision(state: LeagueStructureState, si: number, di: number, func:(val: DivisionState) => DivisionState) { - return replaceSubleague(state, si, subleague => ({ - name: subleague.name, - id: subleague.id, - divisions: replaceIndex(subleague.divisions, di, func(subleague.divisions[di])) - })) + return replaceSubleague(state, si, subleague => { + let nSubleague = shallowClone(subleague); + nSubleague.divisions = replaceIndex(subleague.divisions, di, func(subleague.divisions[di])); + return nSubleague; + }); } +// UTIL + function removeIndex(arr: any[], index: number) { return arr.slice(0, index).concat(arr.slice(index+1)); } @@ -132,21 +154,22 @@ function arrayOf(length: number, func: (i: number) => T): T[] { return out; } -let initLeagueStructure = { - subleagues: [0, 1].map((val) => ({ - name: "", - id: getUID(), - divisions: [0, 1].map((val) => ({ - name: "", - id: getUID(), - teams: [] - })) - })) +function shallowClone(obj: T): T { + return Object.assign({}, obj); } +type DistributiveOmit = T extends any ? Omit : never; + +// CREATE LEAGUE + function CreateLeague() { let [name, setName] = useState(""); + let [showError, setShowError] = useState(false); let [structure, dispatch] = useReducer(leagueStructureReducer, initLeagueStructure); + let gamesSeries = useState('3'); + let seriesDivisionOpp = useState('8'); + let seriesInterDivision = useState('16'); + let seriesInterLeague = useState('8'); let self = useRef(null) @@ -159,30 +182,115 @@ function CreateLeague() { return (
setName(e.target.value)}/> - - +
{name === "" && showError ? "A name is required." : ""}
+ +
+ +
+ +
{ + !validRequest(name, structure, gamesSeries[0], seriesDivisionOpp[0], seriesInterDivision[0], seriesInterLeague[0]) && showError ? + "Cannot create league. Some information is invalid." : "" + }
+
+
); } -function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch}) { +function makeRequest( + name:string, + structure: LeagueStructureState, + gamesPerSeries: string, + divisionSeries: string, + interDivisionSeries: string, + interLeagueSeries: string + ) { + + if (!validRequest(name, structure, gamesPerSeries, divisionSeries, interDivisionSeries, interLeagueSeries)) { + 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 + })) + })) + }, + games_per_series: Number(gamesPerSeries), + division_series: Number(divisionSeries), + inter_division_series: Number(interDivisionSeries), + inter_league_series: Number(interLeagueSeries) + }); +} + +function validRequest( + name:string, + structure: LeagueStructureState, + gamesPerSeries: string, + divisionSeries: string, + interDivisionSeries: string, + interLeagueSeries: string + ) { + + return ( + name !== "" && + validNumber(gamesPerSeries) && + validNumber(divisionSeries) && + validNumber(interDivisionSeries) && + validNumber(interLeagueSeries) && + 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, showError: boolean}) { return (
- - + +
+
{props.state.subleagues.length % 2 !== 0 && props.showError ? "Must have an even number of subleagues." : ""}
); } -function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { +function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch, showError:boolean}) { return (
@@ -192,6 +300,7 @@ function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React. 1} dispatch={action => props.dispatch(Object.assign({subleague_index: i}, action)) }/> +
{subleague.name === "" && props.showError ? "A name is required." : ""}
))} @@ -199,7 +308,7 @@ function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React. ); } -function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit) => void}) { +function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit) => void}) { return (
@@ -210,7 +319,7 @@ function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispa ); } -function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { +function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch, showError: boolean}) { return (<> {props.subleagues[0].divisions.map((val, di) => (
@@ -225,7 +334,7 @@ function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatc
props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) - }/> + } showError={props.showError}/>
))} @@ -234,7 +343,7 @@ function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatc ); } -function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit) => void}) { +function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit) => void, showError:boolean}) { let [newName, setNewName] = useState(""); let [searchResults, setSearchResults] = useState([]); let newNameInput = useRef(null); @@ -248,10 +357,11 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm return (
-
+
props.dispatch({type: 'rename_division', name: e.target.value}) }/> +
{props.state.name === "" && props.showError ? "A name is required." : ""}
{props.state.teams.map((team, i) => (
@@ -283,14 +393,48 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm
):
} +
{props.state.teams.length < 2 && props.showError ? "Must have at least 2 teams." : ""}
); } -function LeagueOptions() { - return ( -
+// LEAGUE OPTIONS +type StateBundle = [T, React.Dispatch>] + +function LeagueOptions(props: { + gamesSeries: StateBundle, + seriesDivisionOpp: StateBundle, + seriesInterDivision: StateBundle, + seriesInterLeague: StateBundle, + showError: boolean + }) { + + let [nGamesSeries, setGamesSeries] = props.gamesSeries; + let [nSeriesDivisionOpp, setSeriesDivisionOpp] = props.seriesDivisionOpp; + let [nSeriesInterDivision, setSeriesInterDivision] = props.seriesInterDivision; + let [nSeriesInterLeague, setSeriesInterLeague] = props.seriesInterLeague; + + return ( +
+
+ + +
+
+ + +
+
+ ); +} + +function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean}) { + return ( +
+
{props.title}
+ props.setValue(e.target.value)}/> +
{(Number(props.value) === NaN || Number(props.value) < 0) && props.showError ? "Must be a number greater than 0" : ""}
); }