Merge pull request #149 from esSteres/leagues
Add create league page to site
This commit is contained in:
commit
60fe1da2ad
|
@ -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 (?,?)"
|
||||
|
|
|
@ -15,7 +15,7 @@ class league_structure(object):
|
|||
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}
|
||||
self.league = league_dic # { subleague name : { division name : [team object] } }
|
||||
self.constraints = {
|
||||
"division_games" : division_games,
|
||||
"inter_div_games" : inter_division_games,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import asyncio, time, datetime, games, json, threading, jinja2, leagues, os
|
||||
from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory
|
||||
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
|
||||
|
||||
app = Flask("the-prestige", static_folder='simmadome/build')
|
||||
app.config['SECRET KEY'] = 'dev'
|
||||
|
@ -16,6 +19,57 @@ def serve(path):
|
|||
else:
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
### API
|
||||
|
||||
@app.route('/api/teams/search')
|
||||
def search_teams():
|
||||
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 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]
|
||||
|
||||
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'))
|
||||
thread2.start()
|
||||
|
|
20
simmadome/package-lock.json
generated
20
simmadome/package-lock.json
generated
|
@ -2276,6 +2276,15 @@
|
|||
"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",
|
||||
|
@ -2370,6 +2379,12 @@
|
|||
"@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",
|
||||
|
@ -9529,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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"@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",
|
||||
|
|
310
simmadome/src/CreateLeague.css
Normal file
310
simmadome/src/CreateLeague.css
Normal file
|
@ -0,0 +1,310 @@
|
|||
.cl_table_header > .cl_subleague_bg {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
height: 2rem;
|
||||
padding-left: 1rem;
|
||||
background: var(--background-secondary);
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
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;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.cl_league_name {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cl_league_options, .cl_league_structure, .cl_confirm_box {
|
||||
display: flex;
|
||||
background: var(--background-tertiary);
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
border-radius: 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.cl_league_structure, .cl_subleague_add_align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.cl_league_structure {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cl_league_structure_table {
|
||||
display: table;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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: 0.5rem 1rem;
|
||||
margin: 0rem 0.5rem;
|
||||
min-width: 22rem;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cl_subleague_name {
|
||||
flex-grow: 1;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.cl_division_name_box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cl_division_name {
|
||||
margin-bottom: 0.5rem;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.cl_newteam_name {
|
||||
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.4rem 0rem;
|
||||
}
|
||||
|
||||
.cl_team_name {
|
||||
font-size: 14pt;
|
||||
padding: 0 0.5rem;
|
||||
overflow: hidden;
|
||||
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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.cl_subleague_delete, .cl_team_delete, .cl_division_delete, .cl_subleague_add, .cl_division_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 {
|
||||
background: var(--accent-green);
|
||||
}
|
||||
|
||||
.cl_subleague_add {
|
||||
position: relative;
|
||||
top: 1.6rem;
|
||||
}
|
||||
|
||||
.cl_division_add {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
492
simmadome/src/CreateLeague.tsx
Normal file
492
simmadome/src/CreateLeague.tsx
Normal file
|
@ -0,0 +1,492 @@
|
|||
import React, {useState, useRef, useLayoutEffect, useReducer} from 'react';
|
||||
import './CreateLeague.css';
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
// STATE CLASSES
|
||||
|
||||
class LeagueStructureState {
|
||||
subleagues: SubleagueState[]
|
||||
|
||||
constructor(subleagues: SubleagueState[] = []) {
|
||||
this.subleagues = subleagues;
|
||||
}
|
||||
}
|
||||
|
||||
class SubleagueState {
|
||||
name: string
|
||||
divisions: DivisionState[]
|
||||
id: string|number
|
||||
|
||||
constructor(divisions: DivisionState[] = []) {
|
||||
this.name = "";
|
||||
this.divisions = divisions;
|
||||
this.id = getUID();
|
||||
}
|
||||
}
|
||||
|
||||
class DivisionState {
|
||||
name: string
|
||||
teams: TeamState[]
|
||||
id: string|number
|
||||
|
||||
constructor() {
|
||||
this.name = "";
|
||||
this.teams = [];
|
||||
this.id = getUID();
|
||||
}
|
||||
}
|
||||
|
||||
class TeamState {
|
||||
name: string
|
||||
id: string|number
|
||||
|
||||
constructor(name: string = "") {
|
||||
this.name = name;
|
||||
this.id = getUID();
|
||||
}
|
||||
}
|
||||
|
||||
let getUID = function() { // does NOT generate UUIDs. Meant to create list keys ONLY
|
||||
let id = 0;
|
||||
return function() { return id++ }
|
||||
}()
|
||||
|
||||
// STRUCTURE REDUCER
|
||||
|
||||
type StructureReducerActions =
|
||||
{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}
|
||||
|
||||
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: append(state.subleagues, new SubleagueState(
|
||||
arrayOf(state.subleagues[0].divisions.length, i =>
|
||||
new DivisionState()
|
||||
)
|
||||
))}
|
||||
case 'rename_subleague':
|
||||
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 => {
|
||||
let nSubleague = shallowClone(subleague);
|
||||
nSubleague.divisions = removeIndex(subleague.divisions, action.division_index)
|
||||
return nSubleague;
|
||||
})};
|
||||
case 'add_divisions':
|
||||
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 => {
|
||||
let nDivision = shallowClone(division);
|
||||
nDivision.name = action.name;
|
||||
return nDivision;
|
||||
});
|
||||
case 'remove_team':
|
||||
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 => {
|
||||
let nDivision = shallowClone(division);
|
||||
nDivision.teams = append(division.teams, new TeamState(action.name));
|
||||
return nDivision;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
let nSubleague = shallowClone(subleague);
|
||||
nSubleague.divisions = replaceIndex(subleague.divisions, di, func(subleague.divisions[di]));
|
||||
return nSubleague;
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return arr.slice(0, index).concat(arr.slice(index+1));
|
||||
}
|
||||
|
||||
function replaceIndex<T>(arr: T[], index: number, val: T) {
|
||||
return arr.slice(0, index).concat([val]).concat(arr.slice(index+1));
|
||||
}
|
||||
|
||||
function append<T>(arr: T[], val: T) {
|
||||
return arr.concat([val]);
|
||||
}
|
||||
|
||||
function arrayOf<T>(length: number, func: (i: number) => T): T[] {
|
||||
var out: T[] = [];
|
||||
for (var i = 0; i < length; i++) {
|
||||
out.push(func(i));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shallowClone<T>(obj: T): T {
|
||||
return Object.assign({}, obj);
|
||||
}
|
||||
|
||||
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
||||
type DistributivePick<T, K extends keyof T> = T extends any ? Pick<T, K> : 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 [nameExists, setNameExists] = useState(false);
|
||||
let [createSuccess, setCreateSuccess] = useState(false);
|
||||
let [structure, structureDispatch] = useReducer(leagueStructureReducer, initLeagueStructure);
|
||||
let [options, optionsDispatch] = useReducer(LeagueOptionsReducer, new LeagueOptionsState());
|
||||
|
||||
let self = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (self.current) {
|
||||
twemoji.parse(self.current)
|
||||
}
|
||||
})
|
||||
|
||||
if (createSuccess) {
|
||||
return(
|
||||
<div className="cl_league_main" ref={self}>
|
||||
<div className="cl_confirm_box">
|
||||
League created succesfully!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cl_league_main" ref={self}>
|
||||
<input type="text" className="cl_league_name" placeholder="League Name" value={name} onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setNameExists(false);
|
||||
}}/>
|
||||
<div className="cl_structure_err">{
|
||||
name === "" && showError ? "A name is required." :
|
||||
nameExists && showError ? "A league by that name already exists" :
|
||||
""
|
||||
}</div>
|
||||
<LeagueStructre state={structure} dispatch={structureDispatch} showError={showError}/>
|
||||
<div className="cl_league_options">
|
||||
<LeagueOptions state={options} dispatch={optionsDispatch} showError={showError}/>
|
||||
<div className="cl_option_submit_box">
|
||||
<button className="cl_option_submit" onClick={e => {
|
||||
if (!validRequest(name, structure, options)) {
|
||||
setShowError(true);
|
||||
} else {
|
||||
let req = new XMLHttpRequest();
|
||||
let data = makeRequest(name, structure, options);
|
||||
req.open("POST", "/api/leagues", true);
|
||||
req.setRequestHeader("Content-type", "application/json");
|
||||
req.onreadystatechange = () => {
|
||||
if(req.readyState === 4) {
|
||||
if (req.status === 200) {
|
||||
setCreateSuccess(true);
|
||||
}
|
||||
if (req.status === 400) {
|
||||
setNameExists(true);
|
||||
setShowError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
req.send(data);
|
||||
|
||||
}
|
||||
}}>Submit</button>
|
||||
<div className="cl_option_err">{
|
||||
!validRequest(name, structure, options) && showError ?
|
||||
"Cannot create league. Some information is missing or invalid." : ""
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function makeRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) {
|
||||
return JSON.stringify({
|
||||
name: name,
|
||||
structure: {
|
||||
subleagues: structure.subleagues.map(subleague => ({
|
||||
name: subleague.name,
|
||||
divisions: subleague.divisions.map(division => ({
|
||||
name: division.name,
|
||||
teams: division.teams.map(team => team.name)
|
||||
}))
|
||||
}))
|
||||
},
|
||||
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, options:LeagueOptionsState) {
|
||||
return (
|
||||
name !== "" &&
|
||||
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, 0) &&
|
||||
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, min = 1) {
|
||||
return Number(value) !== NaN && Number(value) >= min
|
||||
}
|
||||
|
||||
// LEAGUE STRUCUTRE
|
||||
|
||||
function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch<StructureReducerActions>, showError: boolean}) {
|
||||
return (
|
||||
<div className="cl_league_structure">
|
||||
<div className="cl_league_structure_scrollbox">
|
||||
<div className="cl_subleague_add_align">
|
||||
<div className="cl_league_structure_table">
|
||||
<SubleagueHeaders subleagues={props.state.subleagues} dispatch={props.dispatch} showError={props.showError}/>
|
||||
<Divisions subleagues={props.state.subleagues} dispatch={props.dispatch} showError={props.showError}/>
|
||||
</div>
|
||||
<button className="cl_subleague_add" onClick={e => props.dispatch({type: 'add_subleague'})}>➕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cl_structure_err">{props.state.subleagues.length % 2 !== 0 && props.showError ? "Must have an even number of subleagues." : ""}</div>
|
||||
<button className="cl_division_add" onClick={e => props.dispatch({type: 'add_divisions'})}>➕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<StructureReducerActions>, showError:boolean}) {
|
||||
return (
|
||||
<div className="cl_headers">
|
||||
<div key="filler" className="cl_delete_filler"/>
|
||||
{props.subleagues.map((subleague, i) => (
|
||||
<div key={subleague.id} className="cl_table_header">
|
||||
<div className="cl_subleague_bg">
|
||||
<SubleageHeader state={subleague} canDelete={props.subleagues.length > 1} dispatch={action =>
|
||||
props.dispatch(Object.assign({subleague_index: i}, action))
|
||||
}/>
|
||||
<div className="cl_structure_err">{subleague.name === "" && props.showError ? "A name is required." : ""}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit<StructureReducerActions, 'subleague_index'>) => void}) {
|
||||
return (
|
||||
<div className="cl_subleague_header">
|
||||
<input type="text" className="cl_subleague_name" placeholder="Subleague Name" value={props.state.name} onChange={e =>
|
||||
props.dispatch({type: 'rename_subleague', name: e.target.value})
|
||||
}/>
|
||||
{props.canDelete ? <button className="cl_subleague_delete" onClick={e => props.dispatch({type: 'remove_subleague'})}>➖</button> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<StructureReducerActions>, showError: boolean}) {
|
||||
return (<>
|
||||
{props.subleagues[0].divisions.map((val, di) => (
|
||||
<div key={val.id} className="cl_table_row">
|
||||
<div key="delete" className="cl_delete_box">
|
||||
{props.subleagues[0].divisions.length > 1 ?
|
||||
<button className="cl_division_delete" onClick={e => props.dispatch({type: 'remove_divisions', division_index: di})}>➖</button> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{props.subleagues.map((subleague, si) => (
|
||||
<div key={subleague.id} className="cl_division_cell">
|
||||
<div className="cl_subleague_bg">
|
||||
<Division state={subleague.divisions[di]} dispatch={action =>
|
||||
props.dispatch(Object.assign({subleague_index: si, division_index: di}, action))
|
||||
} showError={props.showError}/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>);
|
||||
}
|
||||
|
||||
function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit<StructureReducerActions, 'subleague_index'|'division_index'>) => void, showError:boolean}) {
|
||||
let [newName, setNewName] = useState("");
|
||||
let [searchResults, setSearchResults] = useState<string[]>([]);
|
||||
let newNameInput = useRef<HTMLInputElement>(null);
|
||||
let resultList = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (resultList.current) {
|
||||
twemoji.parse(resultList.current)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="cl_division">
|
||||
<div className="cl_division_name_box">
|
||||
<input type="text" className="cl_division_name" placeholder="Division Name" key="input" value={props.state.name} onChange={e =>
|
||||
props.dispatch({type: 'rename_division', name: e.target.value})
|
||||
}/>
|
||||
<div className="cl_structure_err cl_structure_err_div">{props.state.name === "" && props.showError ? "A name is required." : ""}</div>
|
||||
</div>
|
||||
{props.state.teams.map((team, i) => (
|
||||
<div className="cl_team" key={team.id}>
|
||||
<div className="cl_team_name">{team.name}</div>
|
||||
<button className="cl_team_delete" onClick={e => props.dispatch({type:'remove_team', name: team.name})}>➖</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="cl_team_add">
|
||||
<input type="text" className="cl_newteam_name" placeholder="Add team..." value={newName} ref={newNameInput}
|
||||
onChange={e => {
|
||||
let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'});
|
||||
fetch("/api/teams/search?" + params.toString())
|
||||
.then(response => response.json())
|
||||
.then(data => setSearchResults(data));
|
||||
setNewName(e.target.value);
|
||||
}}/>
|
||||
</div>
|
||||
{searchResults.length > 0 && newName.length > 0 ?
|
||||
(<div className="cl_search_list" ref={resultList}>
|
||||
{searchResults.map(result =>
|
||||
<div className="cl_search_result" key={result} onClick={e => {
|
||||
props.dispatch({type:'add_team', name: result});
|
||||
setNewName("");
|
||||
if (newNameInput.current) {
|
||||
newNameInput.current.focus();
|
||||
}
|
||||
}}>{result}</div>
|
||||
)}
|
||||
</div>):
|
||||
<div/>
|
||||
}
|
||||
<div className="cl_structure_err cl_structure_err_teams">{props.state.teams.length < 2 && props.showError ? "Must have at least 2 teams." : ""}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// LEAGUE OPTIONS
|
||||
|
||||
function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispatch<OptionsReducerActions>, showError: boolean}) {
|
||||
return (
|
||||
<div className="cl_option_main">
|
||||
<div className="cl_option_column">
|
||||
<NumberInput title="Number of games per series" value={props.state.games_series} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_games_series', value: value})} showError={props.showError}/>
|
||||
<NumberInput title="Number of teams from top of division to postseason" value={props.state.top_postseason} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_top_postseason', value: value})} showError={props.showError}/>
|
||||
<NumberInput title="Number of wildcards" value={props.state.wildcards} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_wildcards', value: value})} showError={props.showError}/>
|
||||
</div>
|
||||
<div className="cl_option_column">
|
||||
<NumberInput title="Number of series with each division opponent" value={props.state.intra_division_series} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_intra_division_series', value: value})} showError={props.showError}/>
|
||||
<NumberInput title="Number of inter-divisional series" value={props.state.inter_division_series} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_inter_division_series', value: value})} showError={props.showError}/>
|
||||
<NumberInput title="Number of inter-league series" value={props.state.inter_league_series} setValue={(value: string) =>
|
||||
props.dispatch({type: 'set_inter_league_series', value: value})} showError={props.showError}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean}) {
|
||||
return (
|
||||
<div className="cl_option_box">
|
||||
<div className="cl_option_label">{props.title}</div>
|
||||
<input className="cl_option_input" type="number" min="0" value={props.value} onChange={e => props.setValue(e.target.value)}/>
|
||||
<div className="cl_option_err">{(Number(props.value) === NaN || Number(props.value) < 0) && props.showError ? "Must be a number greater than 0" : ""}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateLeague;
|
|
@ -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(
|
|||
<Header />
|
||||
<Switch>
|
||||
<Route path="/game/:id" component={GamePage}/>
|
||||
<Route path="/create_league" component={CreateLeague} />
|
||||
<Route path="/" component={GamesPage}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
@ -20,6 +22,7 @@ ReactDOM.render(
|
|||
document.getElementById('root')
|
||||
);
|
||||
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div id="header">
|
||||
|
|
Loading…
Reference in New Issue
Block a user