diff --git a/main_controller.py b/main_controller.py index 997a692..26de3b5 100644 --- a/main_controller.py +++ b/main_controller.py @@ -57,6 +57,7 @@ def create_league(): return jsonify({'status':'err_invalid_subleague_division_total'}), 400 league_dic = {} + all_teams = set() err_teams = [] for subleague in config['structure']['subleagues']: if subleague['name'] in league_dic: @@ -67,10 +68,14 @@ def create_league(): if division['name'] in subleague_dic: return jsonify({'status':'err_duplicate_name', 'cause':f"{subleague['name']}/{division['name']}"}), 400 elif len(division['teams']) > MAX_TEAMS_PER_DIVISION: - return jsonify({'status':'err_too_many_teams', 'cause':f"{subleague['name']}/{division['name']}"}) + return jsonify({'status':'err_too_many_teams', 'cause':f"{subleague['name']}/{division['name']}"}), 400 teams = [] for team_name in division['teams']: + if team_name in all_teams: + return jsonify({'status':'err_duplicate_team', 'cause':team_name}), 400 + all_teams.add(team_name) + team = games.get_team(team_name) if team is None: err_teams.append(team_name) diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css index 9aadf3f..1c44259 100644 --- a/simmadome/src/CreateLeague.css +++ b/simmadome/src/CreateLeague.css @@ -255,7 +255,8 @@ input[type=number]::-webkit-outer-spin-button { .cl_structure_err_team { margin-top: -0.5rem; margin-bottom: 0; - width: 85%; + width: 95%; + padding-left: 0.5rem; } .cl_team_name_err { diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index 7f2d530..012eb90 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -183,18 +183,51 @@ function CreateLeague() { let [name, setName] = useState(""); let [showError, setShowError] = useState(false); let [nameExists, setNameExists] = useState(false); - let [deletedTeams, setDeletedTeams] = useState([]); + let [deletedTeams, setDeletedTeams] = useState(new Set()); let [createSuccess, setCreateSuccess] = useState(false); let [structure, structureDispatch] = useReducer(leagueStructureReducer, initLeagueStructure); let [options, optionsDispatch] = useReducer(LeagueOptionsReducer, new LeagueOptionsState()); - let self = useRef(null) + let self = useRef(null); useLayoutEffect(() => { if (self.current) { twemoji.parse(self.current) } - }) + }); + + let duplicateTeams = getDuplicateTeams(structure); + + let submit = () => { + if (!validRequest(name, structure, options)) { + setShowError(true); + } 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) { + let err = JSON.parse(req.response); + switch (err.status) { + case 'err_league_exists': + setNameExists(true); + break; + case 'err_no_such_team': + setDeletedTeams(new Set(err.cause)); + break; + } + setShowError(true); + } + } + } + req.send(data); + } + } if (createSuccess) { return( @@ -217,40 +250,21 @@ function CreateLeague() { nameExists && showError ? "A league by that name already exists" : "" } - +
- +
- +
{ !validRequest(name, structure, options) && showError ? "Cannot create league. Some information is missing or invalid." : "" @@ -283,8 +297,6 @@ function makeRequest(name:string, structure: LeagueStructureState, options:Leagu } function validRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) { - - return ( name !== "" && @@ -296,6 +308,7 @@ function validRequest(name:string, structure: LeagueStructureState, options:Leag validNumber(options.wildcards, 0) && structure.subleagues.length % 2 === 0 && + getDuplicateTeams(structure).size === 0 && structure.subleagues.every((subleague, si) => subleague.name !== "" && @@ -314,9 +327,27 @@ function validNumber(value: string, min = 1) { return !isNaN(Number(value)) && Number(value) >= min; } +function getDuplicateTeams(structure: LeagueStructureState) { + return new Set( + structure.subleagues.map(subleague => + subleague.divisions.map(division => + division.teams.map(team => team.name) + ).reduce((prev, curr) => prev.concat(curr), []) + ).reduce((prev, curr) => prev.concat(curr), []) + .filter((val, i, arr) => arr.slice(0, i).indexOf(val) >= 0) + ) +} + // LEAGUE STRUCUTRE -function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch, deletedTeams: string[], showError: boolean}) { +function LeagueStructre(props: { + state: LeagueStructureState, + dispatch: React.Dispatch, + deletedTeams: Set, + duplicateTeams: Set, + showError: boolean + }) { + let nSubleagues = props.state.subleagues.length; let nDivisions = props.state.subleagues[0].divisions.length; return ( @@ -324,8 +355,18 @@ function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dis
- - + +
{ (nSubleagues+1) * (nDivisions+1) < MAX_SUBLEAGUE_DIVISION_TOTAL ? : @@ -380,7 +421,14 @@ function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispa ); } -function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch, deletedTeams: string[], showError: boolean}) { +function Divisions(props: { + subleagues: SubleagueState[], + dispatch: React.Dispatch, + deletedTeams: Set, + duplicateTeams: Set, + showError: boolean + }) { + return (<> {props.subleagues[0].divisions.map((val, di) => (
@@ -393,11 +441,16 @@ function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatc {props.subleagues.map((subleague, si) => (
- - props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) - } - isDuplicate={subleague.divisions.slice(0, di).some(val => val.name === subleague.divisions[di].name)} - deletedTeams={props.deletedTeams} showError={props.showError} /> + + props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) + } + isDuplicate={subleague.divisions.slice(0, di).some(val => val.name === subleague.divisions[di].name)} + deletedTeams={props.deletedTeams} + duplicateTeams={props.duplicateTeams} + showError={props.showError} + />
))} @@ -410,7 +463,8 @@ function Division(props: { state: DivisionState, dispatch: (action: DistributiveOmit) => void, isDuplicate: boolean, - deletedTeams: string[], + deletedTeams: Set, + duplicateTeams: Set, showError: boolean }) { @@ -442,24 +496,27 @@ function Division(props: { }/>
{props.showError ? divisionErr : ""}
- {props.state.teams.map((team, i) => { - let showDeleted = props.showError && props.deletedTeams.includes(team.name) - return (<> -
-
{team.name}
- -
-
{showDeleted ? "This team was deleted" : ""}
- ) - })} - { props.state.teams.length < MAX_TEAMS_PER_DIVISION ? <> + {props.state.teams.map((team, i) => ( + + ))} + {props.state.teams.length < MAX_TEAMS_PER_DIVISION ? <>
{ - let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'}); - fetch("/api/teams/search?" + params.toString()) - .then(response => response.json()) - .then(data => setSearchResults(data)); + if (e.target.value === "") { + setSearchResults([]); + } else { + let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'}); + fetch("/api/teams/search?" + params.toString()) + .then(response => response.json()) + .then(data => setSearchResults(data)); + } setNewName(e.target.value); }}/>
@@ -485,6 +542,30 @@ function Division(props: { ); } +function Team(props: { + state: TeamState, + dispatch: (action: DistributiveOmit) => void, + isDuplicate: boolean, + isDeleted: boolean, + showError: boolean + }) { + + let errMsg = + props.isDeleted ? + "This team was deleted" : + props.isDuplicate ? + "Each team in a league must be unique" : + ""; + + return (<> +
+
{props.state.name}
+ +
+
{props.showError ? errMsg : ""}
+ ); +} + // LEAGUE OPTIONS function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispatch, showError: boolean}) { diff --git a/simmadome/src/Game.css b/simmadome/src/Game.css index ca73b4d..8388824 100644 --- a/simmadome/src/Game.css +++ b/simmadome/src/Game.css @@ -171,6 +171,8 @@ .update_text { text-align: start; + margin-top: 0.25rem; + margin-bottom: 0.25rem; } .field {