+
+
+ props.dispatch({type: 'rename_division', name: e.target.value})
+ }/>
+
{props.showError ? divisionErr : ""}
+
+ {props.state.teams.map((team, i) => {
+ let showDeleted = props.showError && props.deletedTeams.includes(team.name)
+ return (<>
+
+
{team.name}
+
+
+
{showDeleted ? "This team was deleted" : ""}
+ >)
+ })}
+ { props.state.teams.length < MAX_TEAMS_PER_DIVISION ? <>
+
+ {
+ 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);
+ }}/>
+
+ {searchResults.length > 0 && newName.length > 0 ?
+ (
+ {searchResults.map(result =>
+
{
+ props.dispatch({type:'add_team', name: result});
+ setNewName("");
+ if (newNameInput.current) {
+ newNameInput.current.focus();
+ }
+ }}>{result}
+ )}
+
):
+ null
+ }> :
+ null
+ }
+
+
{props.showError ? teamsErr : ""}
+
+ );
+}
+
+// LEAGUE OPTIONS
+
+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}/>
+
+
+ );
+}
+
+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)}/>
+
{(isNaN(Number(props.value)) || Number(props.value) < minValue) && props.showError ? "Must be a number greater than " + minValue : ""}
+
+ );
+}
+
+export default CreateLeague;
\ No newline at end of file
diff --git a/simmadome/src/Game.css b/simmadome/src/Game.css
new file mode 100644
index 0000000..ca73b4d
--- /dev/null
+++ b/simmadome/src/Game.css
@@ -0,0 +1,213 @@
+.game {
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ background:var(--background-main);
+ border: 4px solid;
+ border-radius: 4px;
+ border-color: var(--highlight);
+ border-top: none;
+ border-right: none;
+ border-bottom: none;
+ height: min-content;
+ width: 100%;
+ min-width: 32rem;
+ max-width: 44rem;
+}
+
+.header {
+ width: 100%;
+ background-color: var(--background-secondary);
+ border-top-right-radius: 4px;
+ height: max-content;
+ display: flex;
+ justify-content: space-between
+}
+
+.header > div {
+ margin: 0.3rem 0.5rem;
+}
+
+.body {
+ margin: 0.5rem;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-areas:
+ "teams teams info" "players players info" "update update update";
+ grid-template-rows: minmax(5.75rem, min-content) minmax(4rem, min-content) minmax(4rem, min-content);
+ grid-row-gap: 0.5rem;
+ grid-column-gap: 0.75rem;
+}
+
+.teams {
+ grid-area: teams;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ min-width: 95%;
+ max-width: 100%;
+ width: min-content;
+}
+
+.team {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ margin: 0.25rem 0rem;
+}
+
+.team_name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.info {
+ grid-area: info;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-around;
+ background: #4f545c;
+ padding: 0.75rem 0rem;
+ border-radius: 4px;
+}
+
+.batting {
+ font-size: 10pt;
+ text-align: left;
+ height: max-content;
+ margin: 0.3rem 0.5rem;
+}
+
+.leagueoruser {
+ font-size: 10pt;
+ text-align: right;
+ height: max-content;
+ margin: 0.3rem 0.5rem;
+}
+
+.footer {
+ display: flex;
+ justify-content: space-between;
+}
+
+.outs {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 60%;
+}
+
+.outs_title {
+ font-weight: bolder;
+}
+
+.outs_count {
+ display: flex;
+}
+
+.out {
+ height: 20px;
+}
+
+.team_name, .score {
+ font-size: 25px
+}
+
+.score {
+ background: var(--background-secondary);
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ margin-left: 10px;
+}
+
+.players {
+ grid-area: players;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ grid-template-rows: auto auto;
+ grid-column-gap: 0.5rem;
+ margin-left: 0.3rem;
+}
+
+.players > div {
+ margin: 0.25rem 0rem;
+}
+
+.player_type {
+ text-align: end;
+ font-weight: bolder;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.player_name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: start;
+ white-space: nowrap;
+ width: 100%;
+}
+
+.update {
+ grid-area: update;
+ min-height: 3.5rem;
+ padding: 0rem 0.75rem;
+ height: 100%;
+ background: var(--background-secondary);
+ border-radius: 4px;
+ align-items: center;
+ display: flex;
+ justify-content: flex-start;
+}
+
+.update_emoji {
+ margin-right: 0.75rem;
+ margin-left: 2px;
+}
+
+.update_text {
+ text-align: start;
+}
+
+.field {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ flex-direction: column;
+}
+
+.base {
+ height: 60px;
+}
+
+.field > .base {
+ margin-bottom: -25%
+}
+
+@media only screen and (max-device-width: 800px) {
+ .game {
+ font-size: 15pt;
+ }
+
+ .batting, .leagueoruser {
+ font-size: 14pt;
+ padding: 3px;
+ }
+
+ .team_name, .score {
+ font-size: 25px
+ }
+
+ .players {
+ font-size: 20px;
+ }
+
+ .update_emoji, .update_text {
+ display: inline;
+ font-size: 20px;
+ }
+}
\ No newline at end of file
diff --git a/simmadome/src/Game.tsx b/simmadome/src/Game.tsx
new file mode 100644
index 0000000..af1a50d
--- /dev/null
+++ b/simmadome/src/Game.tsx
@@ -0,0 +1,85 @@
+import { GameState } from './GamesUtil';
+import twemoji from 'twemoji';
+import React, { useRef, useLayoutEffect } from 'react';
+import { Link } from 'react-router-dom';
+import './Game.css';
+import base_filled from './img/base_filled.png';
+import base_empty from './img/base_empty.png';
+import out_filled from './img/out_out.png';
+import out_empty from './img/out_in.png';
+
+function Game(props: {gameId: string, state : GameState}) {
+ let self: React.MutableRefObject = useRef(null);
+ useLayoutEffect(() => {
+ if (self.current) {
+ twemoji.parse(self.current);
+ }
+ })
+
+ let state = props.state;
+ return (
+
+
+
Inning: {state.display_top_of_inning ? "πΌ" : "π½"} {state.display_inning}/{state.max_innings}
+
{state.title}
+
{state.weather_emoji} {state.weather_text}
+
+
+
+
+
+
+
+
+
+
OUTS
+
+ {[1, 2].map((out) => )}
+
+
+
+
+
PITCHER
+
{state.pitcher}
+
BATTER
+
{state.batter}
+
+
+
{state.update_emoji}
+
{state.update_text}
+
+
+
+
{state.display_top_of_inning ? state.away_name : state.home_name} batting.
+
{state.leagueoruser} (share)
+
+
+ );
+}
+
+function Team(props: {name: string, score: number}) {
+ return (
+
+
{ props.name }
+
{ props.score }
+
+ );
+}
+
+function Base(props: {name: string | null}) {
+ return (
+
+ );
+}
+
+function Out(props: {thisOut: number, totalOuts: number}) {
+ return
;
+}
+
+export default Game;
\ No newline at end of file
diff --git a/simmadome/src/GamePage.css b/simmadome/src/GamePage.css
new file mode 100644
index 0000000..e07cd9a
--- /dev/null
+++ b/simmadome/src/GamePage.css
@@ -0,0 +1,37 @@
+#game_container {
+ margin-top: 3rem;
+ margin-left: 1rem;
+ margin-right: 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-around;
+}
+
+.history_box {
+ width: 100%;
+ margin-top: 3rem;
+ padding: 1rem;
+ padding-top: 0.5rem;
+ background: var(--background-main);
+ border-radius: 0.25rem;
+ width: 100%;
+ min-width: 32rem;
+ max-width: 44rem;
+ box-sizing: border-box;
+ border: 4px solid;
+ border-radius: 4px;
+ border-color: var(--highlight);
+ border-top: none;
+ border-right: none;
+ border-bottom: none;
+}
+
+.history_title {
+ font-size: 14pt;
+}
+
+.history_update {
+ height: 4rem;
+ margin: 0.5rem;
+}
\ No newline at end of file
diff --git a/simmadome/src/GamePage.tsx b/simmadome/src/GamePage.tsx
new file mode 100644
index 0000000..cdf2c31
--- /dev/null
+++ b/simmadome/src/GamePage.tsx
@@ -0,0 +1,62 @@
+import React, {useState, useRef, useLayoutEffect} from 'react';
+import twemoji from 'twemoji';
+import ReactRouter from 'react-router';
+import {GameState, useListener} from './GamesUtil';
+import './GamePage.css';
+import Game from './Game';
+import {getUID} from './util';
+
+function GamePage(props: ReactRouter.RouteComponentProps<{id: string}>) {
+ let [game, setGame] = useState<[string, GameState]|undefined>(undefined);
+ let history = useRef<[number, string, string][]>([]);
+
+ useListener((newGames) => {
+ let newGame = newGames.find((gamePair) => gamePair[0] === props.match.params.id);
+ setGame(newGame);
+ console.log(newGame);
+ if (newGame !== undefined && newGame[1].start_delay < 0 && newGame[1].end_delay > 8) {
+ history.current.unshift([getUID(), newGame[1].update_emoji, newGame[1].update_text]);
+ if (history.current.length > 8) {
+ history.current.pop();
+ }
+ }
+ });
+
+ if (game === undefined) {
+ return The game you're looking for either doesn't exist or has already ended.
+ }
+
+ return (
+
+
+ { history.current.length > 0 ?
+ :
+ null
+ }
+
+ );
+}
+
+function GameHistory(props: {history: [number, string, string][]}) {
+ let self = useRef(null);
+
+ useLayoutEffect(() => {
+ if (self.current) {
+ twemoji.parse(self.current);
+ }
+ })
+
+ return (
+
+
History
+ {props.history.map((update) => (
+
+
{update[1]}
+
{update[2]}
+
+ ))}
+
+ );
+}
+
+export default GamePage;
\ No newline at end of file
diff --git a/simmadome/src/GamesPage.css b/simmadome/src/GamesPage.css
new file mode 100644
index 0000000..8baef47
--- /dev/null
+++ b/simmadome/src/GamesPage.css
@@ -0,0 +1,79 @@
+.container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(32rem, 1fr));
+ grid-gap: 50px 30px; /*space between rows, then columns*/
+ grid-auto-flow: row;
+}
+
+.slot_container {
+ display: flex;
+ justify-content: space-around;
+}
+
+.emptyslot, .game {
+ min-height: 18.75rem;
+ max-width: 44rem;
+ min-width: 32rem;
+ width: 100%;
+}
+
+#filters {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 20px;
+}
+
+#filters > * {
+ padding: 0.25rem 0.5rem;
+ margin: 0rem 0.5rem;
+ font-size: 16pt;
+ background: rgba(0,0,0,0);
+}
+
+#filters > .filter {
+ border-radius: 0.5rem;
+ min-width: 6.25rem;
+ text-align: center;
+ border: none;
+ color: white;
+ text-decoration: none;
+}
+
+#selected_filter {
+ background: rgb(113, 54, 138);
+}
+
+#footer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 100%;
+ height: 4.5rem;
+}
+
+#footer > div {
+ text-align: center;
+ font-size: 16pt;
+ position: relative;
+ top: 0.25rem;
+}
+
+.emptyslot {
+ border: 2px dashed white;
+ border-radius: 15px;
+ text-align: center;
+ color: white;
+}
+
+@media only screen and (max-device-width: 800px) {
+ .container {
+ display: grid;
+ grid-template-columns: repeat(1, minmax(700px, 90%));
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, 0);
+ }
+}
diff --git a/simmadome/src/GamesPage.tsx b/simmadome/src/GamesPage.tsx
new file mode 100644
index 0000000..4fdaacb
--- /dev/null
+++ b/simmadome/src/GamesPage.tsx
@@ -0,0 +1,142 @@
+import React, {useState, useRef, useEffect, useLayoutEffect} from 'react';
+import {GameState, GameList, useListener} from './GamesUtil';
+import {Link} from 'react-router-dom';
+import './GamesPage.css';
+import Game from './Game';
+
+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);
+ let filter = searchparams.get('league') ?? ""
+
+ let [games, setGames] = useState<[string, GameState][]>([]);
+ useListener(setGames);
+
+ let filters = useRef(filter !== "" ? [filter] : []);
+ games.forEach((game) => { if (game[1].is_league && !filters.current.includes(game[1].leagueoruser)) { filters.current.push(game[1].leagueoruser) }});
+ filters.current = filters.current.filter((f) => games.find((game) => game && game[1].is_league && game[1].leagueoruser === f) || f === filter);
+
+ let gameList = useRef<(string | null)[]>([]);
+ let filterGames = games.filter((game, i) => filter === "" || game[1].leagueoruser === filter);
+ updateList(gameList.current, filterGames, searchparams.get('game'));
+
+ return (
+ <>
+
+ val !== null ? filterGames.find((game) => game[0] === val) as [string, GameState] : null )}/>
+