From 72edbf50229fde090f9ed74d53fb4ffb6e32fa56 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Sun, 10 Jan 2021 01:27:30 -0500 Subject: [PATCH 01/39] make league structure --- simmadome/src/CreateLeague.css | 177 +++++++++++++++++++++++ simmadome/src/CreateLeague.tsx | 250 +++++++++++++++++++++++++++++++++ simmadome/src/index.tsx | 2 + 3 files changed, 429 insertions(+) create mode 100644 simmadome/src/CreateLeague.css create mode 100644 simmadome/src/CreateLeague.tsx diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css new file mode 100644 index 0000000..e867503 --- /dev/null +++ b/simmadome/src/CreateLeague.css @@ -0,0 +1,177 @@ +th, td { + border: none; + padding: 0; + height: 100%; +} + +table { + border-collapse: collapse; + height: min-content; +} + +th .cl_subleague_bg { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +tbody tr:last-child .cl_subleague_bg { + border-bottom-left-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; +} + +input { + border: none; + border-radius: 1rem; + height: 2rem; + padding-left: 1rem; + background: var(--background-secondary); + font-size: 14pt; +} + +.cl_league_main { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin-top: 3rem; +} + +.cl_league_name { + margin: 1rem; +} + +.cl_league_options, .cl_league_structure { + display: flex; + background: var(--background-tertiary); + flex-direction: column; + max-width: 100%; + border-radius: 1rem; + padding-top: 1.5rem; +} + +.cl_league_structure, .cl_subleague_add_align { + display: flex; + align-items: center; + justify-content: center; + width: min-content; +} + +.cl_league_structure_table { + margin: 1rem; + margin-left: 0rem; + margin-top: 0rem; +} + +.cl_league_structure_scrollbox { + max-width: 100%; + overflow-y: scroll; +} + + /* Hide scrollbar for Chrome, Safari and Opera */ +.cl_league_structure_scrollbox::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.cl_league_structure_scrollbox { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.cl_subleague_add_align{ + margin-left: 1.5rem; + padding-right: 1.5rem; +} + +.cl_subleague_header { + display: flex; + width:100%; + align-items: center; + justify-content: space-between; +} + +.cl_subleague_bg { + background: var(--background-main); + padding:1rem; + padding-bottom: 0rem; + margin: 0rem 0.5rem; + height: 100%; + width: 20rem; +} + +.cl_subleague_name, .cl_newteam_name { + flex-grow: 1; + margin-right: 0.5rem; +} + +.cl_division_name { + margin-bottom: 0.5rem; + width: 95%; +} + +.cl_division { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + border-radius: 0.5rem; + background: var(--background-accent); +} + +.cl_team, .cl_team_add { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin: 0.25rem 0rem; +} + +.cl_team_name { + font-size: 14pt; + padding-right: 0.5rem; + overflow: hidden; + text-overflow: ellipsis; +} + +/* button styles */ + +button > .emoji { + margin: 0; +} + +.cl_subleague_delete, .cl_team_delete, .cl_division_delete, .cl_subleague_add, .cl_division_add, .cl_newteam_add { + padding: 0; + width: 2rem; + height: 2rem; + border: none; + border-radius: 1rem; + display: flex; + align-items: center; + justify-content: center; +} + +.cl_subleague_delete, .cl_team_delete, .cl_division_delete { + background: var(--accent-red); +} + +.cl_subleague_add, .cl_division_add, .cl_newteam_add { + background: var(--accent-green); +} + +.cl_subleague_add { + margin-top: 3rem; +} + +.cl_division_add { + margin: 1.25rem; +} + +.cl_division_delete { + margin-right: 1rem; + margin-top: 1rem; +} + +.cl_delete_filler { + min-width: 3rem; +} \ No newline at end of file diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx new file mode 100644 index 0000000..a64c4d8 --- /dev/null +++ b/simmadome/src/CreateLeague.tsx @@ -0,0 +1,250 @@ +import React, {useState, useRef, useLayoutEffect, useReducer} from 'react'; +import './CreateLeague.css'; +import twemoji from 'twemoji'; + +interface LeagueStructureState { + subleagues: SubleagueState[] +} + +interface SubleagueState { + name: string + divisions: DivisionState[] +} + +interface DivisionState { + name: string + teams: TeamState[] +} + +interface TeamState { + name: string +} + +let initLeagueStructure = { + subleagues: [0, 1].map((val) => ({ + name: "", + divisions: [0, 1].map((val) => ({ + name: "", + teams: [] + })) + })) +} + +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 extends any ? Omit : never; + +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: "", + divisions: arrayOf(state.subleagues[0].divisions.length, i => ({ + name: "", + teams: [] + })) + }])}; + case 'rename_subleague': + return replaceSubleague(state, action.subleague_index, subleague => ({ + name: action.name, + divisions: subleague.divisions + })); + case 'remove_divisions': + return {subleagues: state.subleagues.map(subleague => ({ + name: subleague.name, + divisions: removeIndex(subleague.divisions, action.division_index) + }))}; + case 'add_divisions': + return {subleagues: state.subleagues.map(subleague => ({ + name: subleague.name, + divisions: subleague.divisions.concat([{name: "", teams: []}]) + }))}; + case 'rename_division': + return replaceDivision(state, action.subleague_index, action.division_index, division => ({ + name: action.name, + teams: division.teams + })); + case 'remove_team': + return replaceDivision(state, action.subleague_index, action.division_index, division => ({ + name: division.name, + 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, + teams: division.teams.concat([{name: action.name}]) + })); + } +} + +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, + 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(arr: T[], index: number, val: T) { + return arr.slice(0, index).concat([val]).concat(arr.slice(index+1)); +} + +function append(arr: T[], val: T) { + return arr.concat([val]); +} + +function arrayOf(length: number, func: (i: number) => T): T[] { + var out: T[] = []; + for (var i = 0; i < length; i++) { + out.push(func(i)); + } + return out; +} + +function CreateLeague() { + let [name, setName] = useState(""); + let [structure, dispatch] = useReducer(leagueStructureReducer, initLeagueStructure); + + let self = useRef(null) + + useLayoutEffect(() => { + if (self.current) { + twemoji.parse(self.current) + } + }) + + return ( +
+ setName(e.target.value)}/> + + +
+ ); +} + +function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch}) { + return ( +
+
+
+ + + +
+ +
+
+ +
+ ); +} + +function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { + return ( + + + + {props.subleagues.map((subleague, i) => ( + +
+ 1} dispatch={action => + props.dispatch(Object.assign({subleague_index: i}, action)) + }/> +
+ + ))} + + + ); +} + +function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit) => void}) { + return ( +
+ + props.dispatch({type: 'rename_subleague', name: e.target.value}) + }/> + {props.canDelete ? : null} +
+ ); +} + +function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { + return ( + + {props.subleagues[0].divisions.map((val, di) => ( + + + {props.subleagues[0].divisions.length > 1 ? + : + null + } + + {props.subleagues.map((subleague, si) => ( + +
+ + props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) + }/> +
+ + ))} + + ))} + + ); +} + +function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit) => void}) { + let [newName, setNewName] = useState(""); + + return ( +
+ + props.dispatch({type: 'rename_division', name: e.target.value}) + }/> + {props.state.teams.map((team, i) => ( +
+
{team.name}
+ +
+ ))} +
+ setNewName(e.target.value)}/> + +
+
+ ); +} + +function LeagueOptions() { + return ( +
+
+ +
+
+ ); +} + +export default CreateLeague; \ No newline at end of file diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index 1235dcc..21bd0f7 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -4,6 +4,7 @@ import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import './index.css'; import GamesPage from './GamesPage'; import GamePage from './GamePage'; +import CreateLeague from './CreateLeague'; import discordlogo from "./img/discord.png"; import reportWebVitals from './reportWebVitals'; @@ -13,6 +14,7 @@ ReactDOM.render(
+ From c3a35c07a4726e535d2ec25cdf2ec6cc2f10ccca Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Sun, 10 Jan 2021 23:47:49 -0500 Subject: [PATCH 02/39] work on team search --- main_controller.py | 24 ++++- simmadome/package-lock.json | 87 ++++++++++++++--- simmadome/package.json | 4 +- simmadome/src/CreateLeague.css | 101 ++++++++++++++------ simmadome/src/CreateLeague.tsx | 168 +++++++++++++++++++++------------ simmadome/src/index.tsx | 30 +++--- 6 files changed, 293 insertions(+), 121 deletions(-) diff --git a/main_controller.py b/main_controller.py index 707c9f7..3702495 100644 --- a/main_controller.py +++ b/main_controller.py @@ -1,6 +1,7 @@ import asyncio, time, datetime, games, json, threading, jinja2, leagues, os -from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory +from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory, abort from flask_socketio import SocketIO, emit +import database as db app = Flask("the-prestige", static_folder='simmadome/build') app.config['SECRET KEY'] = 'dev' @@ -16,6 +17,27 @@ def serve(path): else: return send_from_directory(app.static_folder, 'index.html') +### API + +@app.route('/api/teams/search') +def searchteams(): + query = request.args.get('query') + page_len = int(request.args.get('page_len')) + page_num = int(request.args.get('page_num')) + + if query is None: + abort(400, "A query term is required") + + result = db.search_teams(query) + if page_len is not None: #pagination should probably be doen in the sqlite query but this will do for now + if page_num is None: + abort(400, "A page_len argument must be accompanied by a page_num argument") + result = result[page_num*page_len : (page_num + 1)*page_len] + + return jsonify([json.loads(x[0])['name'] for x in result]) #currently all we need is the name but that can change + + +### SOCKETS thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0')) thread2.start() diff --git a/simmadome/package-lock.json b/simmadome/package-lock.json index cccf2db..a27cb55 100644 --- a/simmadome/package-lock.json +++ b/simmadome/package-lock.json @@ -1974,6 +1974,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.1.tgz", "integrity": "sha512-6BU7vAjKuMspCy9QQEtbWgmkuXi/yOSZo3ANdvZmNQW8N/WQGjO9cvlcA5EFJaPtp2hL1RAaPGpCXxumijUxCg==", + "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1989,6 +1990,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -1997,6 +1999,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2006,6 +2009,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -2013,17 +2017,20 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -2034,6 +2041,7 @@ "version": "5.11.8", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.8.tgz", "integrity": "sha512-ScyKrWQM5xNcr79PkSewnA79CLaoxVskE+f7knTOhDD9ftZSA1Jw8mj+pneqhEu3x37ncNfW84NUr7lqK+mXjA==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -2049,6 +2057,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -2057,6 +2066,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2066,6 +2076,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -2073,12 +2084,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, "requires": { "inherits": "^2.0.4", "source-map": "^0.6.1", @@ -2088,17 +2101,20 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-resolve": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0" @@ -2108,6 +2124,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -2118,6 +2135,7 @@ "version": "11.2.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.2.tgz", "integrity": "sha512-jaxm0hwUjv+hzC+UFEywic7buDC9JQ1q3cDsrWVSDAPmLotfA6E6kUHlYm/zOeGCac6g48DR36tFHxl7Zb+N5A==", + "dev": true, "requires": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.28.1" @@ -2127,6 +2145,7 @@ "version": "12.6.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.6.0.tgz", "integrity": "sha512-FNEH/HLmOk5GO70I52tKjs7WvGYckeE/SrnLX/ip7z2IGbffyd5zOUM1tZ10vsTphqm+VbDFI0oaXu0wcfQsAQ==", + "dev": true, "requires": { "@babel/runtime": "^7.12.5" } @@ -2139,7 +2158,8 @@ "@types/aria-query": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", - "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==" + "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==", + "dev": true }, "@types/babel__core": { "version": "7.1.12", @@ -2217,7 +2237,8 @@ "@types/history": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", - "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==" + "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", + "dev": true }, "@types/html-minifier-terser": { "version": "5.1.1", @@ -2249,11 +2270,21 @@ "version": "26.0.19", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.19.tgz", "integrity": "sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ==", + "dev": true, "requires": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" } }, + "@types/jquery": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz", + "integrity": "sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -2292,7 +2323,8 @@ "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true }, "@types/q": { "version": "1.5.4", @@ -2303,6 +2335,7 @@ "version": "16.14.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.2.tgz", "integrity": "sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==", + "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2312,6 +2345,7 @@ "version": "16.9.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz", "integrity": "sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==", + "dev": true, "requires": { "@types/react": "^16" } @@ -2320,6 +2354,7 @@ "version": "5.1.10", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.10.tgz", "integrity": "sha512-yu11Hu16CfGvvBWc7wluRlxbwfuSlY0snEntbbOTvfgMvyO6uLaEpAbnVOntr+9TNIpR++OOlPkmDcJPxOXRaQ==", + "dev": true, "requires": { "@types/history": "*", "@types/react": "*" @@ -2329,6 +2364,7 @@ "version": "5.1.7", "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz", "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==", + "dev": true, "requires": { "@types/history": "*", "@types/react": "*", @@ -2343,10 +2379,17 @@ "@types/node": "*" } }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", + "dev": true + }, "@types/socket.io-client": { "version": "1.4.34", "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.34.tgz", - "integrity": "sha512-Lzia5OTQFJZJ5R4HsEEldywiiqT9+W2rDbyHJiiTGqOcju89sCsQ8aUXDljY6Ls33wKZZGC0bfMhr/VpOyjtXg==" + "integrity": "sha512-Lzia5OTQFJZJ5R4HsEEldywiiqT9+W2rDbyHJiiTGqOcju89sCsQ8aUXDljY6Ls33wKZZGC0bfMhr/VpOyjtXg==", + "dev": true }, "@types/source-list-map": { "version": "0.1.2", @@ -2367,6 +2410,7 @@ "version": "5.9.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", "integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==", + "dev": true, "requires": { "@types/jest": "*" } @@ -2374,7 +2418,8 @@ "@types/twemoji": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@types/twemoji/-/twemoji-12.1.1.tgz", - "integrity": "sha512-dW1B1WHTfrWmEzXb/tp8xsZqQHAyMB9JwLwbBqkIQVzmNUI02R7lJqxUpKFM114ygNZHKA1r74oPugCAiYHt1A==" + "integrity": "sha512-dW1B1WHTfrWmEzXb/tp8xsZqQHAyMB9JwLwbBqkIQVzmNUI02R7lJqxUpKFM114ygNZHKA1r74oPugCAiYHt1A==", + "dev": true }, "@types/uglify-js": { "version": "3.11.1", @@ -4598,7 +4643,8 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true }, "cssdb": { "version": "4.4.0", @@ -4769,7 +4815,8 @@ "csstype": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", - "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true }, "cyclist": { "version": "1.0.1", @@ -5090,7 +5137,8 @@ "dom-accessibility-api": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", - "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==" + "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", + "dev": true }, "dom-converter": { "version": "0.2.0", @@ -9496,6 +9544,11 @@ } } }, + "jquery": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9818,7 +9871,8 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true }, "magic-string": { "version": "0.25.7", @@ -9994,7 +10048,8 @@ "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true }, "mini-create-react-context": { "version": "0.4.1", @@ -12670,6 +12725,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "requires": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -14279,6 +14335,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "requires": { "min-indent": "^1.0.0" } diff --git a/simmadome/package.json b/simmadome/package.json index c98a967..104db30 100644 --- a/simmadome/package.json +++ b/simmadome/package.json @@ -4,6 +4,7 @@ "private": true, "proxy": "http://localhost:5000", "dependencies": { + "jquery": "^3.5.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router": "^5.2.0", @@ -14,11 +15,12 @@ "typescript": "^4.1.3", "web-vitals": "^0.2.4" }, - "devDependencies" : { + "devDependencies": { "@testing-library/jest-dom": "^5.11.8", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.6.0", "@types/jest": "^26.0.19", + "@types/jquery": "^3.5.5", "@types/node": "^12.19.12", "@types/react": "^16.14.2", "@types/react-dom": "^16.9.10", diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css index e867503..b37ea6e 100644 --- a/simmadome/src/CreateLeague.css +++ b/simmadome/src/CreateLeague.css @@ -1,22 +1,17 @@ -th, td { - border: none; - padding: 0; - height: 100%; -} - -table { - border-collapse: collapse; - height: min-content; -} - -th .cl_subleague_bg { +.cl_table_header > .cl_subleague_bg { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; + padding-top: 1rem; } -tbody tr:last-child .cl_subleague_bg { +.cl_league_structure_table .cl_table_row:last-child .cl_subleague_bg { border-bottom-left-radius: 0.5rem; border-bottom-right-radius: 0.5rem; + padding-bottom: 1rem; +} + +.cl_league_structure_table .cl_table_row:last-child .cl_division_delete { + margin-bottom: 0.5rem; } input { @@ -28,6 +23,10 @@ input { font-size: 14pt; } +input:focus { + outline: none; +} + .cl_league_main { display: flex; flex-direction: column; @@ -57,9 +56,23 @@ input { } .cl_league_structure_table { - margin: 1rem; - margin-left: 0rem; - margin-top: 0rem; + display: table; + margin-right: 1rem; + table-layout: fixed; +} + +.cl_headers, .cl_table_row { + display: table-row; + height: min-content; +} + +.cl_table_header, .cl_delete_filler, .cl_delete_box, .cl_division_cell { + display: table-cell; + height:100%; +} + +.cl_delete_box { + vertical-align: middle; } .cl_league_structure_scrollbox { @@ -92,14 +105,14 @@ input { .cl_subleague_bg { background: var(--background-main); - padding:1rem; - padding-bottom: 0rem; + padding: 0.5rem 1rem; margin: 0rem 0.5rem; - height: 100%; - width: 20rem; + width: 22rem; + height:100%; + box-sizing: border-box; } -.cl_subleague_name, .cl_newteam_name { +.cl_subleague_name { flex-grow: 1; margin-right: 0.5rem; } @@ -109,6 +122,10 @@ input { width: 95%; } +.cl_newteam_name { + width: 95%; +} + .cl_division { display: flex; flex-direction: column; @@ -124,7 +141,7 @@ input { align-items: center; justify-content: space-between; width: 100%; - margin: 0.25rem 0rem; + margin: 0.4rem 0rem; } .cl_team_name { @@ -134,13 +151,38 @@ input { text-overflow: ellipsis; } +.cl_team_add { + display: flex; + flex-direction: column; + margin-bottom: 0rem; +} + +.cl_search_list { + width: 95%; + margin-top: 0.6rem; + padding: 0.5rem; + background: var(--background-tertiary); + border-radius: 0.5rem; +} + +.cl_search_result { + padding: 0.2rem 0.4rem; + border-radius: 0.5rem; +} + +.cl_search_result:hover { + background: var(--background-main); +} + /* button styles */ button > .emoji { margin: 0; + width: 1rem; + height: 1rem; } -.cl_subleague_delete, .cl_team_delete, .cl_division_delete, .cl_subleague_add, .cl_division_add, .cl_newteam_add { +.cl_subleague_delete, .cl_team_delete, .cl_division_delete, .cl_subleague_add, .cl_division_add { padding: 0; width: 2rem; height: 2rem; @@ -155,21 +197,18 @@ button > .emoji { background: var(--accent-red); } -.cl_subleague_add, .cl_division_add, .cl_newteam_add { +.cl_subleague_add, .cl_division_add { background: var(--accent-green); } .cl_subleague_add { - margin-top: 3rem; + position: relative; + top: 1.6rem; } .cl_division_add { - margin: 1.25rem; -} - -.cl_division_delete { - margin-right: 1rem; - margin-top: 1rem; + margin-top: 1.25rem; + margin-bottom: 1.25rem; } .cl_delete_filler { diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index a64c4d8..6a275c1 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -1,6 +1,7 @@ import React, {useState, useRef, useLayoutEffect, useReducer} from 'react'; import './CreateLeague.css'; import twemoji from 'twemoji'; +import $, {getJSON} from 'jquery'; interface LeagueStructureState { subleagues: SubleagueState[] @@ -8,26 +9,19 @@ interface LeagueStructureState { interface SubleagueState { name: string + id: string|number divisions: DivisionState[] } interface DivisionState { name: string + id: string|number teams: TeamState[] } interface TeamState { name: string -} - -let initLeagueStructure = { - subleagues: [0, 1].map((val) => ({ - name: "", - divisions: [0, 1].map((val) => ({ - name: "", - teams: [] - })) - })) + id: string|number } type LeagueReducerActions = @@ -42,6 +36,11 @@ type LeagueReducerActions = 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 { switch (action.type) { case 'remove_subleague': @@ -49,40 +48,55 @@ function leagueStructureReducer(state: LeagueStructureState, action: LeagueReduc case 'add_subleague': return {subleagues: state.subleagues.concat([{ name: "", + id: getUID(), divisions: arrayOf(state.subleagues[0].divisions.length, i => ({ name: "", + id: getUID(), teams: [] })) }])}; case 'rename_subleague': return replaceSubleague(state, action.subleague_index, subleague => ({ name: action.name, + id: subleague.id, divisions: subleague.divisions })); case 'remove_divisions': return {subleagues: state.subleagues.map(subleague => ({ name: subleague.name, + id: subleague.id, divisions: removeIndex(subleague.divisions, action.division_index) }))}; case 'add_divisions': return {subleagues: state.subleagues.map(subleague => ({ name: subleague.name, - divisions: subleague.divisions.concat([{name: "", teams: []}]) + id: subleague.id, + divisions: subleague.divisions.concat([{ + name: "", + id: getUID(), + teams: [] + }]) }))}; case 'rename_division': return replaceDivision(state, action.subleague_index, action.division_index, division => ({ name: action.name, + id: division.id, teams: division.teams })); 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)) })); case 'add_team': return replaceDivision(state, action.subleague_index, action.division_index, division => ({ name: division.name, - teams: division.teams.concat([{name: action.name}]) + id: division.id, + teams: division.teams.concat([{ + name: action.name, + id: getUID() + }]) })); } } @@ -94,6 +108,7 @@ 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])) })) } @@ -118,6 +133,18 @@ 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 CreateLeague() { let [name, setName] = useState(""); let [structure, dispatch] = useReducer(leagueStructureReducer, initLeagueStructure); @@ -144,10 +171,10 @@ function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dis
- +
-
+
@@ -158,20 +185,18 @@ function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dis function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { return ( - - - - {props.subleagues.map((subleague, i) => ( - -
- 1} dispatch={action => - props.dispatch(Object.assign({subleague_index: i}, action)) - }/> -
- - ))} - - +
+
+ {props.subleagues.map((subleague, i) => ( +
+
+ 1} dispatch={action => + props.dispatch(Object.assign({subleague_index: i}, action)) + }/> +
+
+ ))} +
); } @@ -187,33 +212,40 @@ function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispa } function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch}) { - return ( - - {props.subleagues[0].divisions.map((val, di) => ( - - - {props.subleagues[0].divisions.length > 1 ? - : - null - } - - {props.subleagues.map((subleague, si) => ( - -
- - props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) - }/> -
- - ))} - - ))} - - ); + return (<> + {props.subleagues[0].divisions.map((val, di) => ( +
+
+ {props.subleagues[0].divisions.length > 1 ? + : + null + } +
+ {props.subleagues.map((subleague, si) => ( +
+
+ + props.dispatch(Object.assign({subleague_index: si, division_index: di}, action)) + }/> +
+
+ ))} +
+ ))} + ); } function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit) => void}) { let [newName, setNewName] = useState(""); + let [searchResults, setSearchResults] = useState([]); + let newNameInput = useRef(null); + let resultList = useRef(null); + + useLayoutEffect(() => { + if (resultList.current) { + twemoji.parse(resultList.current) + } + }) return (
@@ -221,18 +253,36 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm props.dispatch({type: 'rename_division', name: e.target.value}) }/> {props.state.teams.map((team, i) => ( -
+
{team.name}
))}
- setNewName(e.target.value)}/> - + { + let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'}); + $.getJSON("/api/teams/search?" + params.toString(), data => { + console.log(data); + setSearchResults(data); + }) + setNewName(e.target.value); + }}/>
+ {searchResults.length > 0 && newName.length > 0 ? + (
+ {searchResults.map(result => +
{ + props.dispatch({type:'add_team', name: result}); + setNewName(""); + if (newNameInput.current) { + newNameInput.current.focus(); + } + }}>{result}
+ )} +
): +
+ }
); } @@ -240,9 +290,7 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm function LeagueOptions() { return (
-
- -
+
); } diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index 21bd0f7..7d7b6d2 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -7,20 +7,24 @@ import GamePage from './GamePage'; import CreateLeague from './CreateLeague'; import discordlogo from "./img/discord.png"; import reportWebVitals from './reportWebVitals'; +import $ from 'jquery' + +$(document).ready(function() { + ReactDOM.render( + + +
+ + + + + + + , + document.getElementById('root') + ); +}); -ReactDOM.render( - - -
- - - - - - - , - document.getElementById('root') -); function Header() { return ( From 74c9da684f0ace4cfeb7dbfd7c926729575e0c37 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Mon, 11 Jan 2021 14:14:54 -0500 Subject: [PATCH 03/39] change to using fetch over jquery --- simmadome/src/CreateLeague.css | 1 - simmadome/src/CreateLeague.tsx | 8 +++----- simmadome/src/index.tsx | 29 +++++++++++++---------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css index b37ea6e..5d6971b 100644 --- a/simmadome/src/CreateLeague.css +++ b/simmadome/src/CreateLeague.css @@ -58,7 +58,6 @@ input:focus { .cl_league_structure_table { display: table; margin-right: 1rem; - table-layout: fixed; } .cl_headers, .cl_table_row { diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index 6a275c1..718c0bf 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -1,7 +1,6 @@ import React, {useState, useRef, useLayoutEffect, useReducer} from 'react'; import './CreateLeague.css'; import twemoji from 'twemoji'; -import $, {getJSON} from 'jquery'; interface LeagueStructureState { subleagues: SubleagueState[] @@ -262,10 +261,9 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm { let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'}); - $.getJSON("/api/teams/search?" + params.toString(), data => { - console.log(data); - setSearchResults(data); - }) + fetch("/api/teams/search?" + params.toString()) + .then(response => response.json()) + .then(data => setSearchResults(data)); setNewName(e.target.value); }}/>
diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index 7d7b6d2..4f7e32a 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -7,23 +7,20 @@ import GamePage from './GamePage'; import CreateLeague from './CreateLeague'; import discordlogo from "./img/discord.png"; import reportWebVitals from './reportWebVitals'; -import $ from 'jquery' -$(document).ready(function() { - ReactDOM.render( - - -
- - - - - - - , - document.getElementById('root') - ); -}); +ReactDOM.render( + + +
+ + + + + + + , + document.getElementById('root') +); function Header() { From 1337df4af3227bf921280a92d4577a1187306b62 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Mon, 11 Jan 2021 16:11:35 -0500 Subject: [PATCH 04/39] fix weird gap --- simmadome/src/CreateLeague.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index 718c0bf..5751f78 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -248,9 +248,11 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm return (
- - props.dispatch({type: 'rename_division', name: e.target.value}) - }/> +
+ + props.dispatch({type: 'rename_division', name: e.target.value}) + }/> +
{props.state.teams.map((team, i) => (
{team.name}
From 39e30c38114751e1a00c203543167078d425abf2 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Tue, 12 Jan 2021 03:17:02 -0500 Subject: [PATCH 05/39] finish league create page --- simmadome/src/CreateLeague.css | 94 +++++++++- simmadome/src/CreateLeague.tsx | 324 ++++++++++++++++++++++++--------- 2 files changed, 324 insertions(+), 94 deletions(-) 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" : ""}
); } From ab488c331a67a76e31d31a36a94f205610bc9f1e Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Tue, 12 Jan 2021 13:16:31 -0500 Subject: [PATCH 06/39] add more option fields --- simmadome/src/CreateLeague.tsx | 154 +++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 66 deletions(-) diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index f0e4df6..b64ce1e 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -51,14 +51,6 @@ let getUID = function() { // does NOT generate UUIDs. Meant to create list keys return function() { return id++ } }() -let initLeagueStructure = { - subleagues: [0, 1].map((val) => - new SubleagueState([0, 1].map((val) => - new DivisionState() - )) - ) -}; - // STRUCTURE REDUCER type StructureReducerActions = @@ -132,6 +124,50 @@ function replaceDivision(state: LeagueStructureState, si: number, di: number, fu }); } +// 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 +} + // UTIL function removeIndex(arr: any[], index: number) { @@ -159,17 +195,23 @@ function shallowClone(obj: T): T { } type DistributiveOmit = T extends any ? Omit : never; +type DistributivePick = T extends any ? Pick : never; // CREATE LEAGUE +let initLeagueStructure = { + subleagues: [0, 1].map((val) => + new SubleagueState([0, 1].map((val) => + new DivisionState() + )) + ) +}; + 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 [structure, structureDispatch] = useReducer(leagueStructureReducer, initLeagueStructure); + let [options, optionsDispatch] = useReducer(LeagueOptionsReducer, new LeagueOptionsState()); let self = useRef(null) @@ -183,25 +225,19 @@ function CreateLeague() {
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." : "" + !validRequest(name, structure, options) && showError ? + "Cannot create league. Some information is missing or invalid." : "" }
@@ -209,16 +245,9 @@ function CreateLeague() { ); } -function makeRequest( - name:string, - structure: LeagueStructureState, - gamesPerSeries: string, - divisionSeries: string, - interDivisionSeries: string, - interLeagueSeries: string - ) { +function makeRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) { - if (!validRequest(name, structure, gamesPerSeries, divisionSeries, interDivisionSeries, interLeagueSeries)) { + if (!validRequest(name, structure, options)) { return null } @@ -233,28 +262,25 @@ function makeRequest( })) })) }, - games_per_series: Number(gamesPerSeries), - division_series: Number(divisionSeries), - inter_division_series: Number(interDivisionSeries), - inter_league_series: Number(interLeagueSeries) + 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) }); } -function validRequest( - name:string, - structure: LeagueStructureState, - gamesPerSeries: string, - divisionSeries: string, - interDivisionSeries: string, - interLeagueSeries: string - ) { +function validRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) { return ( name !== "" && - validNumber(gamesPerSeries) && - validNumber(divisionSeries) && - validNumber(interDivisionSeries) && - validNumber(interLeagueSeries) && + 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) && structure.subleagues.length % 2 === 0 && structure.subleagues.every(subleague => subleague.name !== "" && @@ -402,28 +428,24 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm 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; - +function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispatch, showError: boolean}) { return (
- - + + props.dispatch({type: 'set_games_series', value: value})} showError={props.showError}/> + + props.dispatch({type: 'set_top_postseason', value: value})} showError={props.showError}/> + + props.dispatch({type: 'set_wildcards', value: value})} showError={props.showError}/>
- - + + props.dispatch({type: 'set_intra_division_series', value: value})} showError={props.showError}/> + + props.dispatch({type: 'set_inter_division_series', value: value})} showError={props.showError}/> + + props.dispatch({type: 'set_inter_league_series', value: value})} showError={props.showError}/>
); From 1189fb219abd7a1206b6c365b316368e1bf65a7b Mon Sep 17 00:00:00 2001 From: Astrid Date: Wed, 13 Jan 2021 12:56:00 +0100 Subject: [PATCH 07/39] Update Dockerfile for frontend build --- .dockerignore | 6 ++++++ .gitignore | 2 ++ Dockerfile | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5c484d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +venv/ +matteo_env/ +__pycache__/ +simmadome/node_modules +data/ +.git/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index b4a7181..af99f24 100644 --- a/.gitignore +++ b/.gitignore @@ -344,6 +344,7 @@ config.json games_config.json weather_config.json ids +data/ # database matteo.db @@ -351,5 +352,6 @@ matteo.db-wal matteo.db-shm /data/leagues/* /matteo_env/Lib/site-packages/flask_socketio/__init__.py +Pipfile env diff --git a/Dockerfile b/Dockerfile index 75d8a22..fc250eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,20 @@ -FROM python:3.8 -EXPOSE 5000 - +# - Build stage 1: frontend (simmadome/ directory) +FROM node:alpine AS frontend WORKDIR /app -COPY . ./ +COPY simmadome/package.json simmadome/package-lock.json ./ +RUN npm install +COPY simmadome/ ./ +RUN npm run build + +# - Build stage 2: backend (Python) +FROM python:3.8 +EXPOSE 5000 +WORKDIR /app + +COPY requirements.txt . RUN pip install -r requirements.txt +COPY . ./ +COPY --from=frontend /app/build/ simmadome/build/ CMD ["python", "the_prestige.py"] From cbcb950d781613dadd424c322250d938ee90ada9 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Wed, 13 Jan 2021 19:20:58 -0500 Subject: [PATCH 08/39] implemented league saving and loading --- games.py | 2 +- league_storage.py | 26 +++++++++ leagues.py | 44 ++++++++++++-- the_prestige.py | 146 +++++++++++++++++++++++++++------------------- 4 files changed, 151 insertions(+), 67 deletions(-) diff --git a/games.py b/games.py index 6b2c99c..d917bca 100644 --- a/games.py +++ b/games.py @@ -173,7 +173,7 @@ class team(object): if rotation_slot is None: self.pitcher = random.choice(temp_rotation) else: - self.pitcher = temp_rotation[rotation_slot % len(temp_rotation)] + self.pitcher = temp_rotation[(rotation_slot-1) % len(temp_rotation)] def is_ready(self): try: diff --git a/league_storage.py b/league_storage.py index 902b372..63a7fe7 100644 --- a/league_storage.py +++ b/league_storage.py @@ -78,6 +78,7 @@ def init_league_db(league): "game_length" : league.game_length, "series_length" : league.series_length, "games_per_hour" : league.games_per_hour, + "owner" : None, "historic" : False } if not os.path.exists(os.path.dirname(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"))): @@ -88,6 +89,20 @@ def init_league_db(league): conn.commit() conn.close() +def save_league(league): + if league_exists(league.name): + state_dic = { + "day" : league.day, + "schedule" : league.schedule, + "game_length" : league.game_length, + "series_length" : league.series_length, + "games_per_hour" : league.games_per_hour, + "owner" : league.owner, + "historic" : league.historic + } + with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file: + json.dump(state_dic, state_file, indent=4) + def add_stats(league_name, player_game_stats_list): conn = create_connection(league_name) if conn is not None: @@ -122,6 +137,17 @@ def update_standings(league_name, update_dic): conn.commit() conn.close() +def get_standings(league_name): + if league_exists(league_name): + conn = create_connection(league_name) + if conn is not None: + c = conn.cursor() + + c.execute("SELECT name, wins, losses, run_diff FROM teams",) + standings_array = c.fetchall() + conn.close() + return standings_array + def league_exists(league_name): diff --git a/leagues.py b/leagues.py index 44dcf27..b685d8a 100644 --- a/leagues.py +++ b/leagues.py @@ -10,6 +10,8 @@ league_dir = "leagues" class league_structure(object): def __init__(self, name): self.name = name + self.historic = False + self.owner = None def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2): self.league = league_dic #key: subleague, value: {division : team_name} @@ -33,7 +35,7 @@ class league_structure(object): def last_series_check(self): - return str(math.ceil((self.day)/self.series_length) + 1) in self.schedule.keys() + return str(math.ceil((self.day)/self.series_length) + 1) not in self.schedule.keys() def day_to_series_num(self, day): return math.ceil((self.day)/self.series_length) @@ -164,19 +166,43 @@ class league_structure(object): day = 1 while not scheduled: found = False - if day in self.schedule.keys(): - for game_on_day in self.schedule[day]: + if str(day) in self.schedule.keys(): + for game_on_day in self.schedule[str(day)]: for team in game: if team in game_on_day: found = True if not found: - self.schedule[day].append(game) + self.schedule[str(day)].append(game) scheduled = True else: - self.schedule[day] = [game] + self.schedule[str(day)] = [game] scheduled = True day += 1 + def standings_embed(self): + this_embed = Embed(color=Color.purple(), title=self.name) + standings = {} + for team_name, wins, losses, run_diff in league_db.get_standings(self.name): + standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff} + for subleague in iter(self.league.keys()): + this_embed.add_field(name="Subleague:", value=f"**{subleague}**", inline = False) + for division in iter(self.league[subleague].keys()): + this_embed.add_field(name="Division:", value=f"**{division}**", inline = False) + teams = self.league[subleague][division].copy() + for index in range(0, len(teams)): + this_team = teams[index] + teams[index] = (this_team, standings[teams[index].name]["wins"], standings[teams[index].name]["losses"], standings[teams[index].name]["run_diff"],) + + def sorter(team_in_list): + return (team_in_list[1], team_in_list[3]) + teams.sort(key=sorter, reverse=True) + + for this_team in teams: + this_embed.add_field(name=this_team[0].name, value=f"{this_team[1]} - {this_team[2]} Diff: {this_team[3]}", inline = False) + + this_embed.set_footer(text=f"Standings as of day {self.day-1}") + return this_embed + class tournament(object): def __init__(self, name, team_dic, series_length = 5, finals_series_length = 7, max_innings = 9, id = None, secs_between_games = 300, secs_between_rounds = 600): self.name = name @@ -289,6 +315,8 @@ def save_league(this_league): league_json_string = jsonpickle.encode(this_league.league, keys=True) json.dump(league_json_string, league_file, indent=4) return True + else: + league_db.save_league(this_league) def load_league_file(league_name): if league_db.league_exists(league_name): @@ -298,4 +326,10 @@ def load_league_file(league_name): this_league.league = jsonpickle.decode(json.load(league_file), keys=True, classes=team) with open(os.path.join(data_dir, league_dir, league_name, f"{this_league.name}.state")) as state_file: state_dic = json.load(state_file) + + this_league.day = state_dic["day"] + this_league.schedule = state_dic["schedule"] + this_league.game_length = state_dic["game_length"] + this_league.series_length = state_dic["series_length"] + this_league.owner = state_dic["owner"] return this_league \ No newline at end of file diff --git a/the_prestige.py b/the_prestige.py index a86eabf..a805b93 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1,6 +1,7 @@ import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime import database as db import onomancer as ono +from league_storage import league_exists from the_draft import Draft, DRAFT_ROUNDS from flask import Flask from uuid import uuid4 @@ -137,9 +138,9 @@ class StartGameCommand(Command): league = command.split("\n")[0].split("--league ")[1].split("-")[0].strip() try: if "-d " in command.split("\n")[0]: - day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip())-1 + day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip()) elif "--day " in command.split("\n")[0]: - day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())-1 + day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip()) except ValueError: await msg.channel.send("Make sure you put an integer after the -d flag.") return @@ -724,26 +725,36 @@ class StartDraftCommand(Command): raise SlowDraftError('Too slow') return draft_message -class DebugLeague(Command): - name = "league" +class DebugLeagueStart(Command): + name = "startleague" async def execute(self, msg, command): - league = leagues.league_structure("test2") - league.setup({ - "nL" : { - "nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")], - "nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")] - }, - "aL" : { - "aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")], - "aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")] - } - }, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12) - league.generate_schedule() - leagues.save_league(league) - await start_league_day(msg.channel, league, autoplay = 1) + if not league_exists("test2"): + league = leagues.league_structure("test2") + league.setup({ + "nL" : { + "nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")], + "nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")] + }, + "aL" : { + "aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")], + "aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")] + } + }, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12) + league.generate_schedule() + leagues.save_league(league) + else: + league = leagues.load_league_file("test2") + await start_league_day(msg.channel, league, autoplay = 2) +class DebugLeagueDisplay(Command): + name = "displayleague" + async def execute(self, msg, command): + if league_exists("test2"): + league = leagues.load_league_file("test2") + await msg.channel.send(embed=league.standings_embed()) + commands = [ @@ -772,7 +783,8 @@ commands = [ HelpCommand(), StartDraftCommand(), DraftPlayerCommand(), - DebugLeague() + DebugLeagueStart(), + DebugLeagueDisplay() ] client = discord.Client() @@ -1408,7 +1420,7 @@ def get_team_fuzzy_search(team_name): async def start_league_day(channel, league, autoplay = 1): current_games = [] - games_to_start = league.schedule[league.day_to_series_num(league.day)] + games_to_start = league.schedule[str(league.day_to_series_num(league.day))] if league.game_length is None: game_length = games.config()["default_length"] else: @@ -1417,10 +1429,11 @@ async def start_league_day(channel, league, autoplay = 1): for pair in games_to_start: if pair[0] is not None and pair[1] is not None: away = get_team_fuzzy_search(pair[0]) - away.set_pitcher(rotation_slot=league.day-1) + away.set_pitcher(rotation_slot=league.day) home = get_team_fuzzy_search(pair[1]) + home.set_pitcher(rotation_slot=league.day) - this_game = games.game(away.prepare_for_save().finalize(), home.prepare_for_save().finalize(), length = game_length) + this_game = games.game(away.finalize(), home.finalize(), length = game_length) this_game, state_init = prepare_game(this_game) state_init["is_league"] = True @@ -1439,7 +1452,7 @@ async def start_league_day(channel, league, autoplay = 1): last = True else: - await channel.send(f"The next series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") + await channel.send(f"The day {league.day} series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") last = False await league_day_watcher(channel, league, current_games, config()['simmadome_url']+ext, autoplay, last) @@ -1454,41 +1467,47 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, while league.active: queued_games = [] while len(games_list) > 0: - try: - for i in range(0, len(games_list)): - game, key = games_list[i] - if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 8: - if game.teams['home'].name not in series_results.keys(): - series_results[game.teams["home"].name] = {} - series_results[game.teams["home"].name]["wins"] = 0 - series_results[game.teams["home"].name]["losses"] = 0 - series_results[game.teams["home"].name]["run_diff"] = 0 - if game.teams['away'].name not in series_results.keys(): - series_results[game.teams["away"].name] = {} - series_results[game.teams["away"].name]["wins"] = 0 - series_results[game.teams["away"].name]["losses"] = 0 - series_results[game.teams["away"].name]["run_diff"] = 0 + #try: + for i in range(0, len(games_list)): + game, key = games_list[i] + if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 8: + if game.teams['home'].name not in series_results.keys(): + series_results[game.teams["home"].name] = {} + series_results[game.teams["home"].name]["wins"] = 0 + series_results[game.teams["home"].name]["losses"] = 0 + series_results[game.teams["home"].name]["run_diff"] = 0 + if game.teams['away'].name not in series_results.keys(): + series_results[game.teams["away"].name] = {} + series_results[game.teams["away"].name]["wins"] = 0 + series_results[game.teams["away"].name]["losses"] = 0 + series_results[game.teams["away"].name]["run_diff"] = 0 - winner_name = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name - loser_name = game.teams['away'].name if game.teams['home'].score > game.teams['away'].score else game.teams['home'].name - rd = int(math.fabs(game.teams['home'].score - game.teams['away'].score)) + winner_name = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name + loser_name = game.teams['away'].name if game.teams['home'].score > game.teams['away'].score else game.teams['home'].name + rd = int(math.fabs(game.teams['home'].score - game.teams['away'].score)) - series_results[winner_name]["wins"] += 1 - series_results[winner_name]["run_diff"] += rd - series_results[loser_name]["losses"] += 1 - series_results[loser_name]["run_diff"] -= rd + series_results[winner_name]["wins"] += 1 + series_results[winner_name]["run_diff"] += rd - league.add_stats_from_game(game.get_team_specific_stats()) + winner_dic = {"wins" : 1, "run_diff" : rd} - final_embed = game_over_embed(game) - await channel.send(f"A {league.name} game just ended!") - await channel.send(embed=final_embed) - if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] < league.series_length: - queued_games.append(game) - games_list.pop(i) - break - except: - print("something went wrong in league_day_watcher") + series_results[loser_name]["losses"] += 1 + series_results[loser_name]["run_diff"] -= rd + + loser_dic = {"losses" : 1, "run_diff" : -rd} + + league.add_stats_from_game(game.get_team_specific_stats()) + league.update_standings({winner_name : winner_dic, loser_name : loser_dic}) + leagues.save_league(league) + final_embed = game_over_embed(game) + await channel.send(f"A {league.name} game just ended!") + await channel.send(embed=final_embed) + if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] < league.series_length: + queued_games.append(game) + games_list.pop(i) + break + #except: + #print("something went wrong in league_day_watcher") await asyncio.sleep(1) league.day += 1 @@ -1499,16 +1518,18 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] for i in range(0, len(validminutes)): if now.minute > validminutes[i]: - if i < len(validminutes)-1: + if i <= len(validminutes)-1: delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) else: delta = datetime.timedelta(minutes= (60 - now.minute)) - next_start = (now + delta).replace(microsecond=0) + next_start = (now + delta).replace(second=0, microsecond=0) wait_seconds = (next_start - now).seconds - - await channel.send(f"The next batch of games for the {league.name} will start in {int(wait_seconds/60)} minutes.") + leagues.save_league(league) + await channel.send(embed=league.standings_embed()) + await channel.send(f"The day {league.day} games for the {league.name} will start in {math.ceil(wait_seconds/60)} minutes.") + leagues.save_league(league) await asyncio.sleep(wait_seconds) await channel.send(f"A {league.name} series is continuing now at {filter_url}") games_list = await continue_league_series(league, queued_games, games_list, series_results) @@ -1516,10 +1537,11 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, league.active = False - league.update_standings(series_results) - if last or autoplay <= 0: #if this series was the last of the season OR number of series to autoplay has been reached - #needs some kind of notification that it's over here + + if last or autoplay == 0: #if this series was the last of the season OR number of series to autoplay has been reached + await channel.send(embed=league.standings_embed()) + await channel.send(f"The {league.name} is no longer autoplaying.") active_leagues.pop(active_leagues.index(league)) return @@ -1536,6 +1558,8 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, next_start = (now + delta).replace(microsecond=0) wait_seconds = (next_start - now).seconds + leagues.save_league(league) + await channel.send(embed=league.standings_embed()) await channel.send(f"""This {league.name} series is now complete! The next series will be starting in {int(wait_seconds/60)} minutes.""") await asyncio.sleep(wait_seconds) From 22115ee2c3d4247542c2b7f61341f2264ed1d6a6 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Wed, 13 Jan 2021 20:55:12 -0500 Subject: [PATCH 09/39] closed #142 --- the_prestige.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/the_prestige.py b/the_prestige.py index a805b93..086c0e0 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1394,7 +1394,6 @@ def game_over_embed(game): title_string += f" with {game.inning - (game.max_innings+1)} extra innings.\n" else: title_string += ".\n" - title_string += game.weather.emoji + game.weather.name winning_team = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name winstring = f"{game.teams['away'].score} to {game.teams['home'].score}\n" @@ -1406,7 +1405,10 @@ def game_over_embed(game): winstring += f"{winning_team} wins!" embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string) - embed.add_field(name="Final score:", value=winstring) + embed.add_field(name="Final score:", value=winstring, inline=False) + embed.add_field(name=f"{game.teams['away'].name} pitcher:", value=game.teams['away'].pitcher.name) + embed.add_field(name=f"{game.teams['home'].name} pitcher:", value=game.teams['home'].pitcher.name) + embed.set_footer(text=game.weather.emoji + game.weather.name) return embed def get_team_fuzzy_search(team_name): From ae256de645b4f456625d67bd0d78a8b5b17bf442 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Wed, 13 Jan 2021 22:34:46 -0500 Subject: [PATCH 10/39] implemented thinned veil and twilight weathers --- games.py | 25 +++++++++++++++++++++---- main_controller.py | 9 +++++++++ the_prestige.py | 2 ++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/games.py b/games.py index d917bca..83104a2 100644 --- a/games.py +++ b/games.py @@ -31,13 +31,14 @@ def config(): def all_weathers(): weathers_dic = { #"Supernova" : weather("Supernova", "🌟"), - "Midnight": weather("Midnight", "🕶"), + #"Midnight": weather("Midnight", "🕶"), "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), - "Heavy Snow": weather("Heavy Snow", "❄") + "Heavy Snow": weather("Heavy Snow", "❄"), + "Twilight" : weather("Twilight", "🌃"), + "Thinned Veil" : weather("Thinned Veil", "🌌") } return weathers_dic - class appearance_outcomes(Enum): strikeoutlooking = "strikes out looking." strikeoutswinging = "strikes out swinging." @@ -269,6 +270,13 @@ class game(object): pb_system_stat = (random.gauss(1*math.erf((bat_stat - pitch_stat)*1.5)-1.8,2.2)) hitnum = random.gauss(2*math.erf(bat_stat/4)-1,3) + if weather.name == "Twilight": + error_line = - (math.log(defender.stlats["defense_stars"] + 1)/50) + 1 + error_roll = random.random() + if error_roll > error_line: + outcome["error"] == True + outcome["defender"] = defender + pb_system_stat = 0.1 if pb_system_stat <= 0: @@ -313,7 +321,7 @@ class game(object): outcome["ishit"] = True if hitnum < 1: outcome["text"] = appearance_outcomes.single - elif hitnum < 2.85: + elif hitnum < 2.85 or "error" in outcome.keys(): outcome["text"] = appearance_outcomes.double elif hitnum < 3.1: outcome["text"] = appearance_outcomes.triple @@ -384,6 +392,11 @@ class game(object): if base is not None: runs += 1 self.bases = {1 : None, 2 : None, 3 : None} + if "veil" in outcome.keys(): + if runs < 4: + self.bases[runs] = self.get_batter() + else: + runs += 1 return runs elif "advance" in outcome.keys(): @@ -535,6 +548,10 @@ class game(object): elif result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: self.get_batter().game_stats["total_bases"] += 4 self.get_batter().game_stats["home_runs"] += 1 + if self.weather.name == "Thinned Veil": + result["veil"] = True + + scores_to_add += self.baserunner_check(defender, result) diff --git a/main_controller.py b/main_controller.py index 73da0a7..215572e 100644 --- a/main_controller.py +++ b/main_controller.py @@ -110,6 +110,8 @@ def update_loop(): if this_game.last_update[0]["defender"] != "": punc = ". " + + if "fc_out" in this_game.last_update[0].keys(): name, base_string = this_game.last_update[0]['fc_out'] updatestring = f"{this_game.last_update[0]['batter']} {this_game.last_update[0]['text'].value.format(name, base_string)} {this_game.last_update[0]['defender']}{punc}" @@ -120,6 +122,13 @@ def update_loop(): state["update_emoji"] = "🏏" state["update_text"] = updatestring + + if "veil" in this_game.last_update[0].keys(): + state["update_emoji"] = "🌌" + state["update_text"] += " {this_game.last_update[0]['batter']}'s will manifests on {games.base_string(this_game.last_update[1])} base!" + elif "error" in this_game.last_update[0].keys(): + state["update_emoji"] = "🌃" + state["update_text"] = f"{this_game.last_update[0]['defender']} lost sight of the ball! {this_game.last_update[0]['batter']} reaches {games.base_string(this_game.last_update[1])} on the error." state["bases"] = this_game.named_bases() diff --git a/the_prestige.py b/the_prestige.py index 086c0e0..9c98076 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1134,6 +1134,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals wins_in_series[winner_name] = 1 final_embed = game_over_embed(game) + final_embed.add_field(name="Series score:", value=f"{wins_in_series[game.teams['away'].name]} - {wins_in_series[game.teams['home'].name]}") await channel.send(f"A {tourney.name} game just ended!") await channel.send(embed=final_embed) if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals: @@ -1502,6 +1503,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, league.update_standings({winner_name : winner_dic, loser_name : loser_dic}) leagues.save_league(league) final_embed = game_over_embed(game) + final_embed.add_field(name="Series score:", value=f"{series_results[game.teams['away'].name]['wins']} - {series_results[game.teams['home'].name]['wins']}") await channel.send(f"A {league.name} game just ended!") await channel.send(embed=final_embed) if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] < league.series_length: From 3def1378a9711b3f9a4ac087f3d517baf7016c96 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Wed, 13 Jan 2021 23:21:28 -0500 Subject: [PATCH 11/39] bugfixes for veil and twilight, changed veil text and emoji --- games.py | 6 +++--- main_controller.py | 8 ++++---- the_prestige.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/games.py b/games.py index 83104a2..5a314c3 100644 --- a/games.py +++ b/games.py @@ -34,7 +34,7 @@ def all_weathers(): #"Midnight": weather("Midnight", "🕶"), "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), "Heavy Snow": weather("Heavy Snow", "❄"), - "Twilight" : weather("Twilight", "🌃"), + "Twilight" : weather("Twilight", "👻"), "Thinned Veil" : weather("Thinned Veil", "🌌") } return weathers_dic @@ -270,11 +270,11 @@ class game(object): pb_system_stat = (random.gauss(1*math.erf((bat_stat - pitch_stat)*1.5)-1.8,2.2)) hitnum = random.gauss(2*math.erf(bat_stat/4)-1,3) - if weather.name == "Twilight": + if self.weather.name == "Twilight": error_line = - (math.log(defender.stlats["defense_stars"] + 1)/50) + 1 error_roll = random.random() if error_roll > error_line: - outcome["error"] == True + outcome["error"] = True outcome["defender"] = defender pb_system_stat = 0.1 diff --git a/main_controller.py b/main_controller.py index 215572e..c66af65 100644 --- a/main_controller.py +++ b/main_controller.py @@ -125,10 +125,10 @@ def update_loop(): if "veil" in this_game.last_update[0].keys(): state["update_emoji"] = "🌌" - state["update_text"] += " {this_game.last_update[0]['batter']}'s will manifests on {games.base_string(this_game.last_update[1])} base!" + state["update_text"] += f" {this_game.last_update[0]['batter']}'s will manifests on {games.base_string(this_game.last_update[1])} base." elif "error" in this_game.last_update[0].keys(): - state["update_emoji"] = "🌃" - state["update_text"] = f"{this_game.last_update[0]['defender']} lost sight of the ball! {this_game.last_update[0]['batter']} reaches {games.base_string(this_game.last_update[1])} on the error." + state["update_emoji"] = "👻" + state["update_text"] = f"{this_game.last_update[0]['batter']}'s hit goes ethereal, and {this_game.last_update[0]['defender']} can't catch it! {this_game.last_update[0]['batter']} reaches base safely." state["bases"] = this_game.named_bases() @@ -150,4 +150,4 @@ def update_loop(): state["update_pause"] -= 1 socketio.emit("states_update", game_states) - time.sleep(1) \ No newline at end of file + time.sleep(4) \ No newline at end of file diff --git a/the_prestige.py b/the_prestige.py index 9c98076..aad0568 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -202,7 +202,7 @@ class StartRandomGameCommand(Command): channel = msg.channel await msg.delete() - await channel.send("Rolling the bones...") + await channel.send("Rolling the bones... This might take a while.") teamslist = games.get_all_teams() game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize()) From 5826574a1d1f23d364d7a4cb4ba530b0ae4e5028 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 14 Jan 2021 00:20:43 -0500 Subject: [PATCH 12/39] startleague command attempt --- league_storage.py | 5 +++-- leagues.py | 4 ++++ main_controller.py | 2 +- the_prestige.py | 44 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/league_storage.py b/league_storage.py index 63a7fe7..dcc4555 100644 --- a/league_storage.py +++ b/league_storage.py @@ -98,7 +98,8 @@ def save_league(league): "series_length" : league.series_length, "games_per_hour" : league.games_per_hour, "owner" : league.owner, - "historic" : league.historic + "historic" : league.historic, + "season" : league.season } with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file: json.dump(state_dic, state_file, indent=4) @@ -154,5 +155,5 @@ def league_exists(league_name): with os.scandir(os.path.join(data_dir, league_dir)) as folder: for subfolder in folder: if league_name in subfolder.name: - return not state(league_name)["historic"] + return True return False \ No newline at end of file diff --git a/leagues.py b/leagues.py index b685d8a..791a1ec 100644 --- a/leagues.py +++ b/leagues.py @@ -12,6 +12,7 @@ class league_structure(object): self.name = name self.historic = False self.owner = None + self.season = 1 def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2): self.league = league_dic #key: subleague, value: {division : team_name} @@ -332,4 +333,7 @@ def load_league_file(league_name): this_league.game_length = state_dic["game_length"] this_league.series_length = state_dic["series_length"] this_league.owner = state_dic["owner"] + this_league.games_per_hour = state_dic["games_per_hour"] + this_league.historic = state_dic["historic"] + this_league.season = state_dic["season"] return this_league \ No newline at end of file diff --git a/main_controller.py b/main_controller.py index c66af65..d2338be 100644 --- a/main_controller.py +++ b/main_controller.py @@ -150,4 +150,4 @@ def update_loop(): state["update_pause"] -= 1 socketio.emit("states_update", game_states) - time.sleep(4) \ No newline at end of file + time.sleep(1) \ No newline at end of file diff --git a/the_prestige.py b/the_prestige.py index aad0568..ef99604 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -726,7 +726,7 @@ class StartDraftCommand(Command): return draft_message class DebugLeagueStart(Command): - name = "startleague" + name = "startdebugleague" async def execute(self, msg, command): if not league_exists("test2"): @@ -743,19 +743,50 @@ class DebugLeagueStart(Command): }, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12) league.generate_schedule() leagues.save_league(league) - else: - league = leagues.load_league_file("test2") - await start_league_day(msg.channel, league, autoplay = 2) class DebugLeagueDisplay(Command): - name = "displayleague" + name = "displaydebugleague" async def execute(self, msg, command): if league_exists("test2"): league = leagues.load_league_file("test2") await msg.channel.send(embed=league.standings_embed()) +class StartLeagueCommand(Command): + name = "startleague" + template = "m;startleague [league name]" + description = """Optional flag: `--queue X` or `-q X` to play X number of series before stopping. +Plays a league with a given name, provided that league has been saved on the website.""" + async def execute(self, msg, command): + league_name = command.split("-").strip() + autoplay = None + + try: + if "--queue " in command: + autoplay = int(command.split("--queue ")[1]) + elif "-q " in command: + autoplay = int(command.split("--queue ")[1]) + if autoplay is not None and autoplay <= 0: + raise ValueError + elif autoplay is None: + autoplay = -1 + except: + await msg.channel.send("Sorry boss, the queue flag needs a natural number. Any whole number over 0 will do just fine.") + return + + if league_exists(league_name): + league = leagues.load_league_file(league_name) + if league.historic: + await msg.channel.send("That league is done and dusted, chief. Sorry.") + return + for active_league in active_leagues: + if active_league.name == league.name: + await msg.channel.send("That league is already running, boss. Patience is a virtue, you know.") + return + await start_league_day(msg.channel, league, autoplay = autoplay) + else: + await msg.channel.send("Couldn't find that league, boss. Did you save it on the website?") commands = [ IntroduceCommand(), @@ -1464,7 +1495,8 @@ async def start_league_day(channel, league, autoplay = 1): async def league_day_watcher(channel, league, games_list, filter_url, autoplay, last = False): league.active = True autoplay -= 1 - active_leagues.append(league) + if league not in active_leagues: + active_leagues.append(league) series_results = {} while league.active: From da8607e2950bd2284d00b03a56c2ae1a19236b5a Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 14 Jan 2021 00:25:24 -0500 Subject: [PATCH 13/39] fixed some blocking bugs in startleague --- league_storage.py | 18 ++---------------- leagues.py | 4 +--- the_prestige.py | 5 +++-- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/league_storage.py b/league_storage.py index dcc4555..c0ad6a6 100644 --- a/league_storage.py +++ b/league_storage.py @@ -72,34 +72,20 @@ def init_league_db(league): for pitcher in team.rotation: c.execute(player_string, (pitcher.name, team.name)) - state_dic = { - "day" : league.day, - "schedule" : league.schedule, - "game_length" : league.game_length, - "series_length" : league.series_length, - "games_per_hour" : league.games_per_hour, - "owner" : None, - "historic" : False - } - if not os.path.exists(os.path.dirname(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"))): - os.makedirs(os.path.dirname(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"))) - with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file: - json.dump(state_dic, state_file, indent=4) - conn.commit() conn.close() def save_league(league): if league_exists(league.name): state_dic = { + "season" : league.season, "day" : league.day, "schedule" : league.schedule, "game_length" : league.game_length, "series_length" : league.series_length, "games_per_hour" : league.games_per_hour, "owner" : league.owner, - "historic" : league.historic, - "season" : league.season + "historic" : league.historic } with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file: json.dump(state_dic, state_file, indent=4) diff --git a/leagues.py b/leagues.py index 791a1ec..fc92657 100644 --- a/leagues.py +++ b/leagues.py @@ -315,9 +315,7 @@ def save_league(this_league): with open(os.path.join(data_dir, league_dir, this_league.name, f"{this_league.name}.league"), "w") as league_file: league_json_string = jsonpickle.encode(this_league.league, keys=True) json.dump(league_json_string, league_file, indent=4) - return True - else: - league_db.save_league(this_league) + league_db.save_league(this_league) def load_league_file(league_name): if league_db.league_exists(league_name): diff --git a/the_prestige.py b/the_prestige.py index ef99604..1c7e2c8 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -759,14 +759,14 @@ class StartLeagueCommand(Command): Plays a league with a given name, provided that league has been saved on the website.""" async def execute(self, msg, command): - league_name = command.split("-").strip() + league_name = command.split("-")[0].strip() autoplay = None try: if "--queue " in command: autoplay = int(command.split("--queue ")[1]) elif "-q " in command: - autoplay = int(command.split("--queue ")[1]) + autoplay = int(command.split("-q ")[1]) if autoplay is not None and autoplay <= 0: raise ValueError elif autoplay is None: @@ -808,6 +808,7 @@ commands = [ SearchTeamsCommand(), StartGameCommand(), StartTournamentCommand(), + StartLeagueCommand(), StartRandomGameCommand(), CreditCommand(), RomanCommand(), From b83bf771cae0516250476d4a90b9df142894afa8 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 14 Jan 2021 00:37:58 -0500 Subject: [PATCH 14/39] made leagues save properly after autoplaying --- the_prestige.py | 1 + 1 file changed, 1 insertion(+) diff --git a/the_prestige.py b/the_prestige.py index 1c7e2c8..fd5ded9 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1579,6 +1579,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, if last or autoplay == 0: #if this series was the last of the season OR number of series to autoplay has been reached await channel.send(embed=league.standings_embed()) await channel.send(f"The {league.name} is no longer autoplaying.") + leagues.save_league(league) active_leagues.pop(active_leagues.index(league)) return From c837a826c0775d72ba3ce24b17318ff601361134 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Thu, 14 Jan 2021 01:30:31 -0500 Subject: [PATCH 15/39] connect league create page to backend --- league_storage.py | 1 + leagues.py | 2 +- main_controller.py | 38 +++++++++++++++++++-- simmadome/src/CreateLeague.css | 11 +++++- simmadome/src/CreateLeague.tsx | 62 ++++++++++++++++++++++++---------- 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/league_storage.py b/league_storage.py index 63a7fe7..5029740 100644 --- a/league_storage.py +++ b/league_storage.py @@ -64,6 +64,7 @@ def init_league_db(league): c.execute(teams_table_check_string) for team in league.teams_in_league(): + print(team) c.execute("INSERT INTO teams (name) VALUES (?)", (team.name,)) player_string = "INSERT INTO stats (name, team_name) VALUES (?,?)" diff --git a/leagues.py b/leagues.py index b685d8a..2d92e7a 100644 --- a/leagues.py +++ b/leagues.py @@ -14,7 +14,7 @@ class league_structure(object): self.owner = None def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2): - self.league = league_dic #key: subleague, value: {division : team_name} + self.league = league_dic # { subleague name : { division name : [team object] } } self.constraints = { "division_games" : division_games, "inter_div_games" : inter_division_games, diff --git a/main_controller.py b/main_controller.py index 0b968e7..8627250 100644 --- a/main_controller.py +++ b/main_controller.py @@ -1,4 +1,6 @@ -import asyncio, time, datetime, games, json, threading, jinja2, leagues, os +import asyncio, time, datetime, games, json, threading, jinja2, leagues, os, leagues +from leagues import league_structure +from league_storage import league_exists from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory, abort from flask_socketio import SocketIO, emit import database as db @@ -20,7 +22,7 @@ def serve(path): ### API @app.route('/api/teams/search') -def searchteams(): +def search_teams(): query = request.args.get('query') page_len = int(request.args.get('page_len')) page_num = int(request.args.get('page_num')) @@ -29,7 +31,7 @@ def searchteams(): abort(400, "A query term is required") result = db.search_teams(query) - if page_len is not None: #pagination should probably be doen in the sqlite query but this will do for now + if page_len is not None: #pagination should probably be done in the sqlite query but this will do for now if page_num is None: abort(400, "A page_len argument must be accompanied by a page_num argument") result = result[page_num*page_len : (page_num + 1)*page_len] @@ -37,6 +39,36 @@ def searchteams(): return jsonify([json.loads(x[0])['name'] for x in result]) #currently all we need is the name but that can change +@app.route('/api/leagues', methods=['POST']) +def create_league(): + config = json.loads(request.data) + + if (league_exists(config['name'])): + abort(400, "A league by that name already exists") + + print(config) + league_dic = { + subleague['name'] : { + division['name'] : [games.get_team(team_name) for team_name in division['teams']] + for division in subleague['divisions'] + } + for subleague in config['structure']['subleagues'] + } + + new_league = league_structure(config['name']) + new_league.setup( + league_dic, + division_games=config['division_series'], + inter_division_games=config['inter_division_series'], + inter_league_games=config['inter_league_series'], + ) + new_league.generate_schedule() + leagues.save_league(new_league) + + return "League created successfully" + + + ### SOCKETS thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0')) diff --git a/simmadome/src/CreateLeague.css b/simmadome/src/CreateLeague.css index 58c3e36..90c680b 100644 --- a/simmadome/src/CreateLeague.css +++ b/simmadome/src/CreateLeague.css @@ -49,7 +49,7 @@ input[type=number]::-webkit-outer-spin-button { margin-top: 1rem; } -.cl_league_options, .cl_league_structure { +.cl_league_options, .cl_league_structure, .cl_confirm_box { display: flex; background: var(--background-tertiary); flex-direction: column; @@ -58,6 +58,15 @@ input[type=number]::-webkit-outer-spin-button { padding-top: 1.5rem; } +.cl_confirm_box { + min-width: 55rem; + padding: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 20pt; +} + .cl_league_options { align-items: center; } diff --git a/simmadome/src/CreateLeague.tsx b/simmadome/src/CreateLeague.tsx index b64ce1e..3a4dbe4 100644 --- a/simmadome/src/CreateLeague.tsx +++ b/simmadome/src/CreateLeague.tsx @@ -210,6 +210,8 @@ let initLeagueStructure = { function CreateLeague() { let [name, setName] = useState(""); let [showError, setShowError] = useState(false); + let [nameExists, setNameExists] = useState(false); + let [createSuccess, setCreateSuccess] = useState(false); let [structure, structureDispatch] = useReducer(leagueStructureReducer, initLeagueStructure); let [options, optionsDispatch] = useReducer(LeagueOptionsReducer, new LeagueOptionsState()); @@ -221,18 +223,52 @@ function CreateLeague() { } }) + if (createSuccess) { + return( +
+
+ League created succesfully! +
+
+ ); + } + return (
- setName(e.target.value)}/> -
{name === "" && showError ? "A name is required." : ""}
+ { + setName(e.target.value); + setNameExists(false); + }}/> +
{ + name === "" && showError ? "A name is required." : + nameExists && showError ? "A league by that name already exists" : + "" + }
{ @@ -246,19 +282,14 @@ function CreateLeague() { } function makeRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) { - - if (!validRequest(name, structure, options)) { - return null - } - - return ({ + return JSON.stringify({ + name: name, structure: { - name: name, subleagues: structure.subleagues.map(subleague => ({ name: subleague.name, divisions: subleague.divisions.map(division => ({ name: division.name, - teams: division.teams + teams: division.teams.map(team => team.name) })) })) }, @@ -272,7 +303,6 @@ function makeRequest(name:string, structure: LeagueStructureState, options:Leagu } function validRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) { - return ( name !== "" && validNumber(options.games_series) && @@ -280,7 +310,7 @@ function validRequest(name:string, structure: LeagueStructureState, options:Leag validNumber(options.inter_division_series) && validNumber(options.inter_league_series) && validNumber(options.top_postseason) && - validNumber(options.wildcards) && + validNumber(options.wildcards, 0) && structure.subleagues.length % 2 === 0 && structure.subleagues.every(subleague => subleague.name !== "" && @@ -292,8 +322,8 @@ function validRequest(name:string, structure: LeagueStructureState, options:Leag ) } -function validNumber(value: string) { - return Number(value) !== NaN && Number(value) > 0 +function validNumber(value: string, min = 1) { + return Number(value) !== NaN && Number(value) >= min } // LEAGUE STRUCUTRE @@ -426,8 +456,6 @@ function Division(props: {state: DivisionState, dispatch:(action: DistributiveOm // LEAGUE OPTIONS -type StateBundle = [T, React.Dispatch>] - function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispatch, showError: boolean}) { return (
From 4acaca0fb19db6797b1f32929a1186c94184f0dc Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 14 Jan 2021 02:37:23 -0500 Subject: [PATCH 16/39] added the startleague command --- the_prestige.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/the_prestige.py b/the_prestige.py index fd5ded9..c34fc97 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1555,7 +1555,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] for i in range(0, len(validminutes)): if now.minute > validminutes[i]: - if i <= len(validminutes)-1: + if i <= len(validminutes)-2: delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) else: delta = datetime.timedelta(minutes= (60 - now.minute)) @@ -1588,7 +1588,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay, validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] for i in range(0, len(validminutes)): if now.minute > validminutes[i]: - if i < len(validminutes)-1: + if i < len(validminutes)-2: delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) else: delta = datetime.timedelta(minutes= (60 - now.minute)) From 96f272127cb9688f47af9a8c0c903df8b9d254e3 Mon Sep 17 00:00:00 2001 From: Elijah Steres Date: Thu, 14 Jan 2021 17:21:31 -0500 Subject: [PATCH 17/39] add create league link, use logos in link_div --- simmadome/src/img/github.png | Bin 0 -> 2330 bytes simmadome/src/img/patreon.png | Bin 0 -> 52210 bytes simmadome/src/img/twitter.png | Bin 0 -> 8862 bytes simmadome/src/index.css | 38 +++++++++++++++++++++++++++++----- simmadome/src/index.tsx | 22 ++++++++++++++++---- 5 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 simmadome/src/img/github.png create mode 100644 simmadome/src/img/patreon.png create mode 100755 simmadome/src/img/twitter.png diff --git a/simmadome/src/img/github.png b/simmadome/src/img/github.png new file mode 100644 index 0000000000000000000000000000000000000000..73db1f61f3aa55fcaecbca896dbea067706bb7bd GIT binary patch literal 2330 zcmaJ@dpy&7A0KVQT+TU3iOLKmvh6n;HrL%O6)ohFIM()K!!EXAIjO}-B}W%^)GMS^ zbRj&Vi`ep%r%r`PE0rauRMJE)p`NWn=lP@Od|$8M_wsqY-=EL>bNS=9n;FC~MOYyq z5Qu5ON?Ne?^wWKYaP5AUm;6k7ER@nCq#?pMX&OrmLdYE9CJ-6GXT^iTAd9m(`4;E} zfy}Yzg+@pt0@sk(LOz-_L`>RbTB#+~b3MT|& z14UGj%UhW)21C+=LfPqwY$6Bc>x1-?k+cr@pp=D_@sk7+lFS?R)h|hV*O@UW6`Ai_H`049j)JozV)4d8r`BUo7M!zgCCD`;G!S0dV&q zp8LL%~Ba#6Eg$iKtZe$N47EeRXa5>+}e1_})f4LZ~7>us9|I_MOlXiY|*KdQX zeS8~zP@tW6v39VXfr*USwp~rS`=B$j`)nk?@}kq(ClF4d>YOz8hkr;=W~kkm zCW7a<(pcz5X#A0R<9Z{E*BfJ?`u2(tHC?l!+(h-PRUM^q=-1CbjbNI^81U7VyU;3( zT2f^?#ma%q8;EN&iz|Pk@=)pdaMq3d25mbDNrIP#bi|hqP`3*fT}kuvR+%ng#;F!o z!3#SBB2F;iNzfJ(L`*fNFT`NdGQiAH|Ej|Hc2f5bF>p2ufwkUZTFe}(dJ$`MX;C(0 z-V{r-E2m(c5BI}drL(~k6SEswne#fEUdvi&3FiZh%N)80&$C_2kh}Zs$!WSMT5)2L z->t!4_PuW(@0iMUf!tM3EXsxR=8aqQJS!tI77vxXD$Q$NZ1vCFp)TCJPPKgJ6$dq6 z6AvwIT>T_kTfhs9Ro&hblgcP`#)4Y!f6RfcKKz5}fFeLswhx__Ru z`f>qsFw=AMC$D2)!n`KC&FVyxLH=oq<|UuIL`hLk2jvOa!Qq_R3CqNXSp|NlZhBlh z@V0BxvHNAmtK__)^rv^SNw9j3M7}#zg*ToWtbKT~2{x#g2)k9TC|R>Dzhq!T-pA~R z>ZL3u2sdInHHePlU?n!;#-16SNWGJd=j+gQ4vI}N_YbJpa3kc$Q-$nCw8DusXXMl4 z*nJS|qzj=rNJpZI; z$vRbg>YS|lmxjj--{P0zyGBQ?yoS4%MX$EkOMqE$O+Pm%_1o-3WM-q=b4f2dQfTt> zOFR@zTq!4JBAQzKEZEFs#YOE+WUR@=e`!;~PCh-+ddeAuP%>I*Cd8Cb%!1YNZFKDIJTra-- zG#+}%TS;ZSZaqDK8^0CL=AU{B|LU>Hu0mU~}a9D5U= zlszxG=SokFC;8liaQHBlh z2BUO}iYG`REj!%$Xj-syeaVfwq3hF5*L>e!Jm^2?Jo?@}>G*`TrXZHmap&CnfJ4u^ zk68!bvsByruv+!zzl{#!R8}8y9^Ra-sENMbl68A9w_o}?z5eO*E&If08t#Rq+K#s{ zb}mq^_${jVjmeHhTXNWUoFMIe)lJ26`vXz zWvk~HR@whs?OF8cKV8YyEs1et|A`i*;GS`_+RH=ns17TKJg|lJv;Y(H<+2}hIXsat z5ANw;3-!HT?J@e?t>VkCV$sSaXTfoVh-!;h=kN@TPi^X~Dlo@yIP4fl2 z#pt2|1Jj>Ov$71^ei~h1H$uM|R6wY=ZG3Q99U>X|mV`BoS9`4vYw~J@(FazM_8hI9o-&aIo@yE| z6xi92RAt7!kM;6HVQV)mjXu!6Snn}L!YlYRm7CF)XL~-WsBd9Uk6GUDFm8S4@oR}s z!f_@&&0G2FHTy?nQd8F&@&}*SrC)xRgU!h6eepUXk}}=;?q@wX1aZ#AJc}avS@*XR MKo6o-Qerdz3wwRZj{pDw literal 0 HcmV?d00001 diff --git a/simmadome/src/img/patreon.png b/simmadome/src/img/patreon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a521e3fd64d841c1499336e26b8004ad21826ca GIT binary patch literal 52210 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 Bh%x{G literal 0 HcmV?d00001 diff --git a/simmadome/src/img/twitter.png b/simmadome/src/img/twitter.png new file mode 100755 index 0000000000000000000000000000000000000000..af44ca5d502e7841fada40eeeefc200d151dc246 GIT binary patch literal 8862 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 NLq%J;LeV_*e*h(XLh%3q literal 0 HcmV?d00001 diff --git a/simmadome/src/index.css b/simmadome/src/index.css index 663440d..9cf2b34 100644 --- a/simmadome/src/index.css +++ b/simmadome/src/index.css @@ -58,23 +58,51 @@ h2 { #link_div { text-align: right; position: absolute; - top: 0px; - right: 30px; + top: 1rem; + right: 2rem; + display: flex; } -#link_div > a { +.github_logo, .twitter_logo, .patreon_container { + height: 2rem; + width: 2rem; + margin-left: 0.75rem; +} + +.patreon_container { + border-radius: 1rem; + background: #FF424D; +} + +.patreon_logo { + box-sizing: border-box; + padding: 0.35rem; + height: 2rem; + width: 2rem; + position: relative; + left: 0.1rem; + bottom: 0.05rem; +} + +a { background-color: transparent; text-decoration: underline; } -#link_div > a:link, #link_div > a:visited { +a:link, a:visited { color: lightblue; } -#link_div > a:hover { +a:hover { color: white; } +#utility_links { + position: absolute; + top: 1rem; + left: 2rem; +} + img.emoji { height: 1em; width: 1em; diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index 4f7e32a..e6a49c9 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -1,12 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; import './index.css'; import GamesPage from './GamesPage'; import GamePage from './GamePage'; import CreateLeague from './CreateLeague'; import discordlogo from "./img/discord.png"; import reportWebVitals from './reportWebVitals'; +import patreonLogo from './img/patreon.png'; +import githubLogo from './img/github.png'; +import twitterLogo from './img/twitter.png'; ReactDOM.render( @@ -27,9 +30,20 @@ function Header() { return (
@@ -479,12 +479,16 @@ function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispat ); } -function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean}) { +function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean, minValue?:number}) { + let minValue = 1; + if (props.minValue !== undefined) { + minValue = props.minValue + } 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" : ""}
+ props.setValue(e.target.value)}/> +
{(!isNaN(Number(props.value)) || Number(props.value) < minValue) && props.showError ? "Must be a number greater than "+minValue : ""}
); } diff --git a/simmadome/src/GamesPage.tsx b/simmadome/src/GamesPage.tsx index 3a2ad97..4fdaacb 100644 --- a/simmadome/src/GamesPage.tsx +++ b/simmadome/src/GamesPage.tsx @@ -8,6 +8,7 @@ function GamesPage() { let [search, setSearch] = useState(window.location.search); useEffect(() => { setSearch(window.location.search); + //eslint-disable-next-line react-hooks/exhaustive-deps }, [window.location.search]) let searchparams = new URLSearchParams(search); diff --git a/simmadome/src/GamesUtil.tsx b/simmadome/src/GamesUtil.tsx index 8002c02..d590399 100644 --- a/simmadome/src/GamesUtil.tsx +++ b/simmadome/src/GamesUtil.tsx @@ -32,6 +32,7 @@ const useListener = (onUpdate: (update: [string, GameState][]) => void, url: str socket.on('connect', () => socket.emit('recieved', {})); socket.on('states_update', onUpdate); return () => {socket.disconnect()}; + //eslint-disable-next-line react-hooks/exhaustive-deps }, [url]) } diff --git a/simmadome/src/index.tsx b/simmadome/src/index.tsx index e6a49c9..b0ec638 100644 --- a/simmadome/src/index.tsx +++ b/simmadome/src/index.tsx @@ -32,14 +32,14 @@ function Header() {