Merge pull request #139 from esSteres/react

Rebuild frontend in react
This commit is contained in:
Sakimori 2021-01-13 02:07:36 -05:00 committed by GitHub
commit c407722166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 17332 additions and 379 deletions

BIN
.DS_Store vendored

Binary file not shown.

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
SOURCES = $(wildcard ./simmadome/src/*) $(wildcard ./simmadome/public/*)
OUTPUTS = $(wildcard ./simmadome/build/*)
.PHONY: run frontend
run: $(OUTPUTS)
python3 the_prestige.py
frontend:
(cd simmadome && npm run build)
$(OUTPUTS): $(SOURCES)
(cd simmadome && npm run build)

View File

@ -23,26 +23,26 @@ accepting pull requests, check the issues for to-dos.
- then another blank line seperating your batters and your pitchers. - then another blank line seperating your batters and your pitchers.
- the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8. - the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8.
- if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and your team will be saved! - if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and your team will be saved!
- m;deleteteam [teamname] (requires team ownership) - m;deleteteam [teamname] \(requires team ownership)
- allows you to delete the team with the provided name. you'll get an embed with a confirmation to prevent accidental deletions. hit the 👍 and your team will be deleted. - allows you to delete the team with the provided name. you'll get an embed with a confirmation to prevent accidental deletions. hit the 👍 and your team will be deleted.
- m;import - m;import
- imports an onomancer collection as a new team. you can use the new onomancer simsim setting to ensure compatibility. similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the 👍 and your team will be saved! - imports an onomancer collection as a new team. you can use the new onomancer simsim setting to ensure compatibility. similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the 👍 and your team will be saved!
#### editing (all of these commands require ownership and exact spelling of the team name): #### editing (all of these commands require ownership and exact spelling of the team name):
- m;addplayer batter/pitcher [team name] [player name] - m;addplayer batter/pitcher [team name] \[player name]
- adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. use addplayer batter or addplayer pitcher at the top of a list with entries separated by new lines: - adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. use addplayer batter or addplayer pitcher at the top of a list with entries separated by new lines:
- the name of the team you want to add the player to. - the name of the team you want to add the player to.
- the name of the player you want to add to the team. - the name of the player you want to add to the team.
- m;moveplayer [team name] [player name] [new lineup/rotation position number] - m;moveplayer [team name] \[player name] [new lineup/rotation position number]
- moves a player within your lineup or rotation. if you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. use this command at the top of a list with entries separated by new lines: - moves a player within your lineup or rotation. if you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. use this command at the top of a list with entries separated by new lines:
- the name of the team you want to move the player on. - the name of the team you want to move the player on.
- the name of the player you want to move. - the name of the player you want to move.
- the position you want to move them too, indexed with 1 being the first position of the lineup or rotation. all players below the specified position in the lineup or rotation will be pushed down. - the position you want to move them too, indexed with 1 being the first position of the lineup or rotation. all players below the specified position in the lineup or rotation will be pushed down.
- m;swapsection [team name] [player name] - m;swapsection [team name] \[player name]
- swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. use this command at the top of a list with entries separated by new lines: - swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. use this command at the top of a list with entries separated by new lines:
- the name of the team you want to swap the player on. - the name of the team you want to swap the player on.
- the name of the player you want to swap. - the name of the player you want to swap.
- m;removeplayer [team name] [player name] - m;removeplayer [team name] \[player name]
- removes a player from your team. if there are multiple copies of the same player on a team this will only delete the first one. use this command at the top of a list with entries separated by new lines: - removes a player from your team. if there are multiple copies of the same player on a team this will only delete the first one. use this command at the top of a list with entries separated by new lines:
- the name of the team you want to remove the player from. - the name of the team you want to remove the player from.
- the name of the player you want to remove. - the name of the player you want to remove.
@ -99,3 +99,7 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them
- Ryan Littleton - Ryan Littleton
- Evie Diver - Evie Diver
- iliana etaoin - iliana etaoin
## Attribution
Twemoji is copyright 2020 Twitter, Inc and other contributors; code licensed under [the MIT License](http://opensource.org/licenses/MIT), graphics licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)

View File

@ -1,36 +1,36 @@
import asyncio, time, datetime, games, json, threading, jinja2, leagues import asyncio, time, datetime, games, json, threading, jinja2, leagues, os
from flask import Flask, url_for, Response, render_template, request, jsonify from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
app = Flask("the-prestige") app = Flask("the-prestige", static_folder='simmadome/build')
app.config['SECRET KEY'] = 'dev' app.config['SECRET KEY'] = 'dev'
#app.config['SERVER_NAME'] = '0.0.0.0:5000' #app.config['SERVER_NAME'] = '0.0.0.0:5000'
socketio = SocketIO(app) socketio = SocketIO(app)
@app.route('/') # Serve React App
def index(): @app.route('/', defaults={'path': ''})
if ('league' in request.args): @app.route('/<path:path>')
return render_template("index.html", league=request.args['league']) def serve(path):
return render_template("index.html") if path != "" and os.path.exists(app.static_folder + '/' + path):
return send_from_directory(app.static_folder, path)
@app.route('/game') else:
def game_page(): return send_from_directory(app.static_folder, 'index.html')
return render_template("game.html")
thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0')) thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0'))
thread2.start() thread2.start()
master_games_dic = {} #key timestamp : (game game, {} state) master_games_dic = {} #key timestamp : (game game, {} state)
data_to_send = [] game_states = []
@socketio.on("recieved") @socketio.on("recieved")
def handle_new_conn(data): def handle_new_conn(data):
socketio.emit("states_update", data_to_send, room=request.sid) socketio.emit("states_update", game_states, room=request.sid)
def update_loop(): def update_loop():
global game_states
while True: while True:
game_states = {} game_states = []
game_ids = iter(master_games_dic.copy().keys()) game_ids = iter(master_games_dic.copy().keys())
for game_id in game_ids: for game_id in game_ids:
this_game, state, discrim_string = master_games_dic[game_id] this_game, state, discrim_string = master_games_dic[game_id]
@ -125,7 +125,7 @@ def update_loop():
state["top_of_inning"] = this_game.top_of_inning state["top_of_inning"] = this_game.top_of_inning
game_states[game_id] = state game_states.append([game_id, state])
if state["update_pause"] <= 1 and state["start_delay"] < 0: if state["update_pause"] <= 1 and state["start_delay"] < 0:
if this_game.over: if this_game.over:
@ -140,17 +140,5 @@ def update_loop():
state["update_pause"] -= 1 state["update_pause"] -= 1
global data_to_send socketio.emit("states_update", game_states)
data_to_send = []
template = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')).get_template('game_box.html')
for id in game_states:
data_to_send.append({
'timestamp' : id,
'league' : game_states[id]['leagueoruser'] if game_states[id]['is_league'] else '',
'state' : game_states[id],
'html' : template.render(state=game_states[id], timestamp=id)
})
socketio.emit("states_update", data_to_send)
time.sleep(8) time.sleep(8)

24
simmadome/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
simmadome/README.md Normal file
View File

@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

16735
simmadome/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
simmadome/package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "simmadome",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"socket.io-client": "^3.0.5",
"twemoji": "^13.0.1",
"typescript": "^4.1.3",
"web-vitals": "^0.2.4"
},
"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/node": "^12.19.12",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/react-router": "^5.1.10",
"@types/react-router-dom": "^5.1.7",
"@types/socket.io-client": "^1.4.34",
"@types/twemoji": "^12.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>⚾ The Simmadome</title>
<meta property="og:title" content="Watch at the Simmadome" />
<meta property="og:description" content="The Simsim: Your players, your teams, your games." />
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@SIBR_XVI">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,11 +1,3 @@
:root {
--background-main: #2f3136; /*discord dark theme background-secondary - the same color as the embeds*/
--background-secondary: #4f545c; /*discord's background-tertiary*/
--background-accent: #4f545c; /*discord's background-accent*/
--highlight: rgb(113, 54, 138); /*matteo purple™*/
}
.game { .game {
align-self: stretch; align-self: stretch;
text-align: center; text-align: center;
@ -125,7 +117,7 @@
} }
.score { .score {
background: var(--background-accent); background: var(--background-secondary);
width: 40px; width: 40px;
min-width: 40px; min-width: 40px;
height: 40px; height: 40px;
@ -170,7 +162,7 @@
border-radius: 4px; border-radius: 4px;
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: start; justify-content: flex-start;
} }
.update_emoji { .update_emoji {
@ -193,7 +185,7 @@
height: 60px; height: 60px;
} }
.base_2 { .field > .base {
margin-bottom: -25% margin-bottom: -25%
} }

85
simmadome/src/Game.tsx Normal file
View File

@ -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<HTMLDivElement | null> = useRef(null);
useLayoutEffect(() => {
if (self.current) {
twemoji.parse(self.current);
}
})
let state = props.state;
return (
<div className="game" ref={self}>
<div className="header">
<div className="inning">Inning: {state.display_top_of_inning ? "🔼" : "🔽"} {state.display_inning}/{state.max_innings}</div>
<div className="title">{state.title}</div>
<div className="weather">{state.weather_emoji} {state.weather_text}</div>
</div>
<div className="body">
<div className="teams">
<Team name={state.away_name} score={state.away_score}/>
<Team name={state.home_name} score={state.home_score}/>
</div>
<div className="info">
<div className="field">
<Base name={state.bases[2]} />
<div style={{display: "flex"}}>
<Base name={state.bases[3]} />
<Base name={state.bases[1]} />
</div>
</div>
<div className="outs">
<div className="outs_title">OUTS</div>
<div className="outs_count">
{[1, 2].map((out) => <Out thisOut={out} totalOuts={state.outs} key={out} />)}
</div>
</div>
</div>
<div className="players">
<div className="player_type">PITCHER</div>
<div className="player_name">{state.pitcher}</div>
<div className="player_type">BATTER</div>
<div className="player_name batter_name">{state.batter}</div>
</div>
<div className="update">
<div className="update_emoji">{state.update_emoji}</div>
<div className="update_text">{state.update_text}</div>
</div>
</div>
<div className="footer">
<div className="batting">{state.display_top_of_inning ? state.away_name : state.home_name} batting.</div>
<div className="leagueoruser">{state.leagueoruser} (<Link to={"/game/" + props.gameId}>share</Link>)</div>
</div>
</div>
);
}
function Team(props: {name: string, score: number}) {
return (
<div className="team">
<div className="team_name">{ props.name }</div>
<div className="score">{ props.score }</div>
</div>
);
}
function Base(props: {name: string | null}) {
return (
<img className="base" alt={ props.name ?? "" } src={ props.name ? base_filled : base_empty }/>
);
}
function Out(props: {thisOut: number, totalOuts: number}) {
return <img className="out" alt="" src={props.thisOut <= props.totalOuts ? out_filled : out_empty}/>;
}
export default Game;

View File

@ -0,0 +1,22 @@
import React, {useState} from 'react';
import ReactRouter from 'react-router';
import {GameState, useListener} from './GamesUtil';
import './GamePage.css';
import Game from './Game';
function GamePage(props: ReactRouter.RouteComponentProps<{id: string}>) {
let [games, setGames] = useState<[string, GameState][]>([]);
useListener((newGames) => setGames(newGames));
let game = games.find((game) => game[0] === props.match.params.id)
return (
<div id="game_container">
{ game ?
<Game gameId={game[0]} state={game[1]}/> :
"The game you're looking for either doesn't exist or has already ended."
}
</div>
);
}
export default GamePage;

View File

@ -5,10 +5,16 @@
grid-auto-flow: row; grid-auto-flow: row;
} }
.slot_container {
display: flex;
justify-content: space-around;
}
.emptyslot, .game { .emptyslot, .game {
min-height: 18.75rem; min-height: 18.75rem;
justify-self: center;
max-width: 44rem; max-width: 44rem;
min-width: 32rem;
width: 100%;
} }
#filters { #filters {
@ -58,8 +64,6 @@
.emptyslot { .emptyslot {
border: 2px dashed white; border: 2px dashed white;
border-radius: 15px; border-radius: 15px;
align-self: stretch;
justify-self: stretch;
text-align: center; text-align: center;
color: white; color: white;
} }

141
simmadome/src/GamesPage.tsx Normal file
View File

@ -0,0 +1,141 @@
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);
}, [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 (
<>
<Filters filterList={filters.current} selectedFilter={filter} />
<Grid gameList={gameList.current.map((val) => val !== null ? filterGames.find((game) => game[0] === val) as [string, GameState] : null )}/>
<Footer has_games={filterGames.length > 0}/>
</>
);
}
// adds and removes games from list to keep it up to date, without relocating games already in place
function updateList(gameList: (string | null)[], games: [string, GameState][], firstGame: string | null) {
// insert firstGame into first slot, if necessary
if (firstGame !== null && games.find((game) => game[0] === firstGame)) {
if (gameList.includes(firstGame)) {
gameList[gameList.indexOf(firstGame)] = null;
}
gameList[0] = firstGame;
}
//remove games no longer present
for (let i = 0; i < gameList.length; i ++) {
if (gameList[i] !== null && games.findIndex((val) => val[0] === gameList[i]) < 0) {
gameList[i] = null;
}
}
// add games not present
for (let game of games) {
if (!gameList.find((val) => val !== null && val === game[0])) {
let firstEmpty = gameList.indexOf(null);
if (firstEmpty < 0) {
gameList.push(game[0])
} else {
gameList[firstEmpty] = game[0];
}
}
}
//remove trailing empty cells
while (gameList[gameList.length-1] === null) {
gameList.pop();
}
}
function Filters (props: {filterList: string[], selectedFilter: string}) {
function Filter(innerprops: {title: string, filter:string} ) {
let search = new URLSearchParams();
search.append('league', innerprops.filter);
return (
<Link to={innerprops.filter !== "" ? "/?" + search.toString() : "/"} className="filter" id={innerprops.filter === props.selectedFilter ? "selected_filter" : ""}>
{innerprops.title}
</Link>
);
}
return (
<div id="filters">
<div>Filter:</div>
<Filter title="All" filter="" key="" />
{props.filterList.map((filter: string) =>
<Filter title={filter} filter={filter} key={filter} />
)}
</div>
);
}
function Grid(props: { gameList: GameList }) {
let self: React.RefObject<HTMLElement> = useRef(null);
let [numcols, setNumcols] = useState(3);
let newList = [...props.gameList];
while (newList.length === 0 || newList.length % numcols !== 0) {
newList.push(null);
}
function getCols() {
if (self.current !== null) {
//this is a hack, but there's weirdly no "real" way to get the number of columns
return window.getComputedStyle(self.current).getPropertyValue('grid-template-columns').split(' ').length;
} else {
return 3;
}
}
//set num cols after page loads, then add listener to update if window resizes
useLayoutEffect(() => {
setNumcols(getCols());
window.addEventListener('resize', (event) => {
setNumcols(getCols());
})
}, [])
let emptyKey = 0;
return (
<section className="container" id="container" ref={self}>
{newList.map((game) => (
<div className="slot_container" key={game ? game[0] : emptyKey++}>
{game ? <Game gameId={game[0]} state={game[1]}/> : <div className="emptyslot"/>}
</div>
))}
</section>
);
}
function Footer(props: { has_games: boolean }) {
let text = props.has_games ? "" : "No games right now. Why not head over to Discord and start one?";
return (
<div id="footer">
<div>{text}</div>
</div>
);
}
export default GamesPage;

View File

@ -0,0 +1,39 @@
import {useLayoutEffect} from 'react';
import io from 'socket.io-client';
interface GameState {
bases: (string | null)[];
outs: number;
display_top_of_inning: boolean
display_inning: number
max_innings: number
title: string
weather_emoji: string
weather_text: string
away_name: string
away_score: number
home_name: string
home_score: number
pitcher: string
batter: string
update_emoji: string
update_text: string
is_league: boolean
leagueoruser: string
}
type GameList = ([id: string, game: GameState] | null)[];
// connects to the given url (or host if none) and waits for state updates
const useListener = (onUpdate: (update: [string, GameState][]) => void, url: string | null = null) => {
useLayoutEffect(() => {
let socket = url ? io(url) : io();
socket.on('connect', () => socket.emit('recieved', {}));
socket.on('states_update', onUpdate);
return () => {socket.disconnect()};
}, [url])
}
export { useListener };
export type { GameState, GameList };

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,11 +1,22 @@
@import url('https://fonts.googleapis.com/css2?family=Alegreya&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Alegreya&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Goldman:wght@700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Goldman:wght@700&display=swap');
:root {
--background-main: #2f3136;
--background-secondary: #4f545c;
--background-tertiary: #202225;
--background-accent: #40444b;
--highlight: rgb(113, 54, 138); /*matteo purple™*/
--accent-red: #f04747;
--accent-green: rgb(67, 181, 129);
}
body { body {
background-image: url("naturalblack.png"); background-image: url("img/naturalblack.png");
} }
/* Background pattern from Toptal Subtle Patterns */ /* Background pattern from Toptal Subtle Patterns */
div, button, h1, h2, a { * {
font-family: 'Alegreya', serif; font-family: 'Alegreya', serif;
color: white; color: white;
} }
@ -65,5 +76,8 @@ h2 {
} }
img.emoji { img.emoji {
height: 14px; height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
} }

40
simmadome/src/index.tsx Normal file
View File

@ -0,0 +1,40 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import './index.css';
import GamesPage from './GamesPage';
import GamePage from './GamePage';
import discordlogo from "./img/discord.png";
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<Router>
<Header />
<Switch>
<Route path="/game/:id" component={GamePage}/>
<Route path="/" component={GamesPage}/>
</Switch>
</Router>
</React.StrictMode>,
document.getElementById('root')
);
function Header() {
return (
<div id="header">
<div id="link_div">
<a href="https://www.patreon.com/sixteen" className="link" target="_blank" rel="noopener noreferrer">Patreon</a><br />
<a href="https://github.com/Sakimori/matteo-the-prestige" className="link" target="_blank" rel="noopener noreferrer">Github</a><br />
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" className="link" target="_blank" rel="noopener noreferrer">Twitter</a>
</div>
<a href="/" className="page_header"><h2 className="page_header" style={{fontSize:"50px"} as React.CSSProperties}>THE SIMMADOME</h2></a>
<h2 className="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" className="link"><img src={discordlogo} alt="" height="30"/></a> to start your own games!</h2>
</div>
);
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
simmadome/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

26
simmadome/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

BIN
static/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

BIN
static/js/.DS_Store vendored

Binary file not shown.

View File

@ -1,25 +0,0 @@
$(document).ready(function (){
var socket = io.connect();
socket.on('connect', function () {
console.log("connected")
socket.emit('recieved', {});
});
socket.on("states_update", function (json) { //json is an object containing all game updates
var searchparams = new URLSearchParams(window.location.search);
var exists = false;
for (game of json) {
if (searchparams.get('timestamp') == game.timestamp) {
$('.game').html(game.html);
exists = true;
}
}
if (!exists) {
$('game').remove()
$('#game_container').text("The game you're looking for either doesn't exist or has already ended.")
}
twemoji.parse(document.body);
});
});

View File

@ -1,187 +0,0 @@
var socket = io.connect();
var lastupdate;
var grid;
$(document).ready(function (){
grid = document.getElementById("container")
socket.on('connect', function () {
socket.emit('recieved', {});
});
socket.on("states_update", function (json) { //json is an object containing all game updates
lastupdate = json;
updateGames(json, $('#selected_filter').text());
updateLeagues(json);
});
});
const updateGames = (json, filter) => {
filterjson = [];
for (var game of json) {
if (game.league == filter || filter == "All") {
filterjson.push(game);
}
}
if (filterjson.length == 0) {
$('#footer div').html("No games right now. Why not head over to Discord and start one?");
} else {
$('#footer div').html("");
}
var searchparams = new URLSearchParams(window.location.search);
if (searchparams.has('game') && filterjson.some(x => x.timestamp == searchparams.get('game')) && grid.children[0].timestamp != searchparams.get('game')) {
var game = filterjson.find(x => x.timestamp == searchparams.get('game'))
var oldbox = Array.prototype.slice.call(grid.children).find(x => x.timestamp == game.timestamp)
if (oldbox) { clearBox(oldbox) }
insertGame(0, game)
}
//replace games that have ended with empty slots
for (var slotnum = 0; slotnum < grid.children.length; slotnum++) {
if (grid.children[slotnum].className == "game" && !filterjson.some((x) => x.timestamp == grid.children[slotnum].timestamp)) {
clearBox(grid.children[slotnum])
}
}
for (var game of filterjson) {
//updates game in list
for (var slotnum = 0; slotnum < grid.children.length; slotnum++) {
if (grid.children[slotnum].timestamp == game.timestamp) {
insertGame(slotnum, game);
};
};
//adds game to list if not there already
if (!Array.prototype.slice.call(grid.children).some(x => x.timestamp == game.timestamp)) {
for (var slotnum = 0; true; slotnum++) { //this is really a while loop but shh don't tell anyone
if (slotnum >= grid.children.length) {
insertEmpty(grid);
}
if (grid.children[slotnum].className == "emptyslot") {
insertGame(slotnum, game);
break;
};
};
}
fillgrid(grid)
};
}
const insertEmpty = (grid) => {
var newBox = document.createElement("DIV");
newBox.className = "emptyslot";
grid.appendChild(newBox);
}
const insertGame = (gridboxnum, game) => {
var thisBox = grid.children[gridboxnum];
thisBox.className = "game";
thisBox.timestamp = game.timestamp;
thisBox.innerHTML = game.html;
twemoji.parse(thisBox);
};
const insertLeague = (league) => {
var btn = document.createElement("BUTTON");
btn.className = "filter";
btn.innerHTML = escapeHtml(league);
$('#filters').append(btn);
return btn;
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
const clearBox = (box) => {
box.className = "emptyslot";
box.timestamp = null;
box.innerHTML = "";
}
const fillgrid = (grid) => {
var gridwidth = window.getComputedStyle(grid).getPropertyValue('grid-template-columns').split(" ").length //hack to get number of grid columns
// add cells to fill last row
while (grid.children.length % gridwidth != 0) {
insertEmpty(grid)
}
//remove last rows if not needed
while (grid.children.length > gridwidth && Array.prototype.slice.call(grid.children).slice(grid.children.length - gridwidth).every( x => x.className == 'emptyslot')) {
for (var i = 0; i < gridwidth; i++) {
grid.removeChild(grid.children[grid.children.length-1]);
}
}
}
const updateLeagues = (games) => {
//get all leagues
var leagues = []
for (var game of games) {
if (game.league != "" && !leagues.includes(game.league)) {
leagues.push(game.league)
}
}
//remove leagues no longer present
$('#filters .filter').each(function(index) {
if (!leagues.includes($(this).text())) {
if (this.id != 'selected_filter' && $(this).text() != "All") { //don't remove the currently selected filter or the "all" filter
$(this).remove();
}
} else {
leagues.splice(leagues.indexOf($(this).text()), 1);
}
})
// add leagues not already present
for (var league of leagues) { // we removed the entries that are already there in the loop above
insertLeague(league)
}
//add click handlers to each filter
$('#filters .filter').each(function(index) {
this.onclick = function() {
if ($('#filters #selected_filter').text() == 'All') {
updateGames([], ""); // clear grid when switching off of All, to make games collapse to top
}
$('#filters #selected_filter').attr('id', '');
this.id = 'selected_filter';
var search = new URLSearchParams();
search.append('league', this.textContent);
history.pushState({}, "", "/" + (this.textContent != 'All' ? "?" + search.toString() : ""));
updateGames(lastupdate, this.textContent);
}
})
}
window.onpopstate = function(e) {
var searchparams = new URLSearchParams(window.location.search);
updateLeagues(lastupdate);
$('#filters #selected_filter').attr('id', '');
if (searchparams.has('league')) {
var filter_found = false
$('#filters .filter').each(function(i) { if (this.textContent == searchparams.get('league')) { this.id = 'selected_filter'; filter_found = true }});
if (!filter_found) { insertLeague(searchparams.get('league')).id = 'selected_filter' }
updateGames(lastupdate, searchparams.get('league'));
} else {
$('#filters .filter').each(function(i) { if (this.textContent == 'All') { this.id = 'selected_filter' }})
updateGames(lastupdate, "All");
}
}
window.addEventListener('resize', function(e) {
fillgrid(grid)
})

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -1,31 +0,0 @@
<html lang="en-US">
<head>
<script src="//code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://twemoji.maxcdn.com/v/latest/twemoji.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.4/socket.io.js"></script>
<script type="text/javascript" async src="https://platform.twitter.com/widgets.js"></script>
<title>⚾ The Simmadome</title>
<meta property="og:title" content="Watch at the Simmadome" />
<meta property="og:description" content="The Simsim: Your players, your teams, your games." />
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@SIBR_XVI">
<link rel="stylesheet" href="/static/css/common.css">
{% block head_tags %}{% endblock %}
</head>
<body>
<div id="header">
<div id="link_div">
<a href="https://www.patreon.com/sixteen" class="link" target="_blank" rel="noopener noreferrer">Patreon</a><br />
<a href="https://github.com/Sakimori/matteo-the-prestige" class="link" target="_blank" rel="noopener noreferrer">Github</a><br />
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" class="link" target="_blank" rel="noopener noreferrer">Twitter</a>
</div>
<a href="/" class="page_header"><h2 class="page_header" style="font-size: 50px;">THE SIMMADOME</h2></a>
<h2 class="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" class="link"><img src="static/discord.png" height="30"></a> to start your own games!</h2>
</div>
{% block body %}{% endblock %}
</body>
</html>

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% block head_tags %}
<link rel="stylesheet" href="/static/css/game.css">
<link rel="stylesheet" href="/static/css/game_page.css">
<script type="text/javascript" src="static/js/game_loader.js"></script>
{% endblock %}
{% block body %}
<div id="game_container">
<div class="game"></div>
</div>
{% endblock %}

View File

@ -1,53 +0,0 @@
{% macro base(number) -%}
src={% if state.bases[number] %}"/static/img/base_filled.png" alt="{{state.bases[number]}}"{% else %}"/static/img/base_empty.png"{% endif %}
{%- endmacro %}
{% macro out(number) -%}
{% if number <= state.outs %}/static/img/out_out.png{% else %}/static/img/out_in.png{% endif %}
{%- endmacro %}
<div class="header">
<div class="inning">Inning: {% if state.display_top_of_inning == true %}🔼{% else %}🔽{% endif %} {{ state.display_inning | escape }}/{{ state.max_innings | escape }}</div>
<div class="title">{{ state.title | escape }}</div>
<div class="weather">{{ state.weather_emoji | escape }} {{ state.weather_text | escape }}</div>
</div>
<div class="body">
<div class="teams">
<div class="team">
<div class="team_name">{{ state.away_name | escape }}</div>
<div class="score">{{ state.away_score }}</div>
</div>
<div class="team">
<div class="team_name">{{ state.home_name | escape }}</div>
<div class="score">{{ state.home_score }}</div>
</div>
</div>
<div class="info">
<div class="field">
<img class="base base_2" {{ base(2) }}/>
<div style="display: flex;">
<img class="base base_3" {{ base(3) }}/>
<img class="base base_1" {{ base(1) }}/>
</div>
</div>
<div class="outs">
<div class="outs_title">OUTS</div>
<div class="outs_count">
<img class="out" src="{{ out(1) }}"/>
<img class="out" src="{{ out(2) }}"/>
</div>
</div>
</div>
<div class="players">
<div class="player_type">PITCHER</div>
<div class="player_name pitcher_name">{{ state.pitcher | escape }}</div>
<div class="player_type">BATTER</div>
<div class="player_name batter_name">{{ state.batter | escape }}</div>
</div>
<div class="update">
<div class="update_emoji">{{ state.update_emoji | escape }}</div>
<div class="update_text">{{ state.update_text | escape }}</div>
</div>
</div>
<div class="footer">
<div class="batting">{% if state.display_top_of_inning == true %}{{ state.away_name | escape }}{% else %}{{ state.home_name | escape }}{% endif %} batting.</div>
<div class="leagueoruser">{{ state.leagueoruser | escape }} (<a href="/game?timestamp={{ timestamp }}">share</a>)</div>
</div>

View File

@ -1,21 +0,0 @@
{% extends "base.html" %}
{% block head_tags %}
<link rel="stylesheet" href="/static/css/games_page.css">
<link rel="stylesheet" href="/static/css/game.css">
<script type="text/javascript" src="static/js/grid_loader.js"></script>
{% endblock %}
{% block body %}
<div id="filters">
<div>Filter:</div>
<button class="filter" {% if not league %}id="selected_filter"{% endif %}>All</button>
{% if league %}<button class="filter" id="selected_filter">{{ league }}</button>{% endif %}
</div>
<section class="container" id="container">
<div class="emptyslot"></div>
<div class="emptyslot"></div>
<div class="emptyslot"></div>
</section>
<div id="footer">
<div></div>
</div>
{% endblock %}