diff --git a/.gitignore b/.gitignore index 01a7145..473c2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -357,3 +357,4 @@ Pipfile env /data/leagues /simmadome/build +/simmadome/.eslintcache diff --git a/leagues.py b/leagues.py index 422c261..96ab48e 100644 --- a/leagues.py +++ b/leagues.py @@ -167,7 +167,7 @@ class league_structure(object): if division_max % 2 != 0: divisions.append(["OFF" for i in range(0, division_max)]) else: - last_div = divisions.pop + last_div = divisions.pop() divs_a = list(chain(divisions[int(len(divisions)/2):]))[0] if last_div is not None: diff --git a/main_controller.py b/main_controller.py index 997a692..0ecfbef 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) @@ -93,7 +98,7 @@ def create_league(): new_league = league_structure(config['name']) new_league.setup( league_dic, - division_games=config['division_series'], # need to add a check that makes sure these values are ok + division_games=config['division_series'], inter_division_games=config['inter_division_series'], inter_league_games=config['inter_league_series'], ) diff --git a/simmadome/.eslintcache b/simmadome/.eslintcache deleted file mode 100644 index 32ab91c..0000000 --- a/simmadome/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/Users/elijah/Documents/Projects/matteo/simmadome/src/index.tsx":"1","/Users/elijah/Documents/Projects/matteo/simmadome/src/reportWebVitals.ts":"2","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesPage.tsx":"3","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamePage.tsx":"4","/Users/elijah/Documents/Projects/matteo/simmadome/src/CreateLeague.tsx":"5","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesUtil.tsx":"6","/Users/elijah/Documents/Projects/matteo/simmadome/src/util.tsx":"7","/Users/elijah/Documents/Projects/matteo/simmadome/src/Game.tsx":"8"},{"size":2368,"mtime":1610663769654,"results":"9","hashOfConfig":"10"},{"size":425,"mtime":1610566206674,"results":"11","hashOfConfig":"10"},{"size":4725,"mtime":1610664926203,"results":"12","hashOfConfig":"10"},{"size":1836,"mtime":1610677519051,"results":"13","hashOfConfig":"10"},{"size":18825,"mtime":1610778204901,"results":"14","hashOfConfig":"10"},{"size":1116,"mtime":1610677473305,"results":"15","hashOfConfig":"10"},{"size":961,"mtime":1610694553519,"results":"16","hashOfConfig":"10"},{"size":3089,"mtime":1610572714752,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1bvn6qu",{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/elijah/Documents/Projects/matteo/simmadome/src/index.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/reportWebVitals.ts",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesPage.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamePage.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/CreateLeague.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesUtil.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/util.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/Game.tsx",[]] \ No newline at end of file diff --git a/simmadome/public/index.html b/simmadome/public/index.html index ddec811..825a173 100644 --- a/simmadome/public/index.html +++ b/simmadome/public/index.html @@ -16,15 +16,5 @@
- 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 { diff --git a/simmadome/src/GamesPage.css b/simmadome/src/GamesPage.css index 8baef47..4ace427 100644 --- a/simmadome/src/GamesPage.css +++ b/simmadome/src/GamesPage.css @@ -70,10 +70,5 @@ @media only screen and (max-device-width: 800px) { .container { - display: grid; - grid-template-columns: repeat(1, minmax(700px, 90%)); - position: absolute; - left: 50%; - transform: translate(-50%, 0); } } diff --git a/simmadome/src/GamesPage.tsx b/simmadome/src/GamesPage.tsx index 4fdaacb..4877f55 100644 --- a/simmadome/src/GamesPage.tsx +++ b/simmadome/src/GamesPage.tsx @@ -5,23 +5,29 @@ import './GamesPage.css'; import Game from './Game'; function GamesPage() { + let gameList = useRef<(string | null)[]>([]); + let [search, setSearch] = useState(window.location.search); useEffect(() => { setSearch(window.location.search); + gameList.current = []; //eslint-disable-next-line react-hooks/exhaustive-deps }, [window.location.search]) + // get filter term let searchparams = new URLSearchParams(search); let filter = searchparams.get('league') ?? "" + // set up socket listener let [games, setGames] = useState<[string, GameState][]>([]); useListener(setGames); + // build filter list let filters = useRef(filter !== "" ? [filter] : []); games.forEach((game) => { if (game[1].is_league && !filters.current.includes(game[1].leagueoruser)) { filters.current.push(game[1].leagueoruser) }}); filters.current = filters.current.filter((f) => games.find((game) => game && game[1].is_league && game[1].leagueoruser === f) || f === filter); - let gameList = useRef<(string | null)[]>([]); + // update game list let filterGames = games.filter((game, i) => filter === "" || game[1].leagueoruser === filter); updateList(gameList.current, filterGames, searchparams.get('game')); diff --git a/simmadome/src/index.css b/simmadome/src/index.css index 9cf2b34..754da75 100644 --- a/simmadome/src/index.css +++ b/simmadome/src/index.css @@ -55,6 +55,12 @@ h2 { margin: auto; } +#links { + display: flex; + width: 100%; + justify-content: space-between; +} + #link_div { text-align: right; position: absolute; @@ -63,6 +69,12 @@ h2 { display: flex; } +#nav_links { + position: absolute; + top: 1rem; + left: 2rem; +} + .github_logo, .twitter_logo, .patreon_container { height: 2rem; width: 2rem; @@ -97,15 +109,18 @@ a:hover { color: white; } -#utility_links { - position: absolute; - top: 1rem; - left: 2rem; -} - img.emoji { height: 1em; width: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; +} + +@media only screen and (max-width: 800px) { + #link_div, #nav_links { + position: static; + margin-top: 0.5rem; + margin-left: 2rem; + margin-right: 2rem; + } } \ No newline at end of file diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index b0ec638..a6f4b73 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -29,6 +29,10 @@ ReactDOM.render( function Header() { return (