Compare commits

...

115 Commits

Author SHA1 Message Date
87b2abbd80 Merge branch 'eventlineup-development' 2024-05-29 16:55:07 -05:00
b0de3fb221 add buttons to move slots
build css
2024-05-29 16:48:03 -05:00
149f0a411b evensheet fix when no availabilities or positions 2024-05-29 16:44:46 -05:00
e6d1d5a697 Merge branch 'simplification' 2024-05-29 16:42:46 -05:00
bf9d0c1a78 simplifications
compile css
2024-05-29 16:39:36 -05:00
64fa16740b enabled 2024-05-27 15:17:27 -05:00
f273677ba7 Merge branch 'eventlineup-availability-reminders'
# Conflicts:
#	src/public/js/eventlineup.js
2024-05-27 15:11:39 -05:00
ca194d516d email fixes, tweaks 2024-05-27 15:10:17 -05:00
ed18389bb2 auto restart of debugger 2024-05-27 15:10:17 -05:00
b346710496 fix embed svg (additional classes) 2024-05-27 15:10:16 -05:00
6d1588e80f statusCode fix 2024-05-27 15:10:16 -05:00
858cb24e3f cleanup
oops missing bracket

cleanup
2024-05-27 15:10:16 -05:00
196eb5f51d fixed adjacent lineup 2024-05-27 15:10:15 -05:00
07446570c1 add clear lineup/availabilities, availability reminders 2024-05-27 15:04:34 -05:00
a5c47ff9a7 Merge branch 'main' into eventlineup-availability-reminders 2024-05-26 11:44:58 -05:00
aa2ebf0b2e Merge branch 'fix-adjacent-lineup' 2024-05-26 11:40:36 -05:00
5f02ea4d5c caddy_data is not external 2024-05-26 11:36:21 -05:00
39380eaf03 persist caddy data, move domain to .env 2024-05-26 11:34:35 -05:00
cb131353dc Merge branch 'docker' 2024-05-26 10:42:45 -05:00
e1c9a7b81b cleanup to allow for debugging 2024-05-26 10:42:21 -05:00
c495b265ee Merge branch 'main' into docker 2024-05-25 19:49:20 -05:00
49864874fc dev script updates for scss watch 2024-05-24 13:40:59 -05:00
e9fd60e619 404 fix 2024-05-24 13:40:42 -05:00
84da372330 Merge branch 'main' into docker 2024-05-24 10:42:19 -05:00
c696ffb4bc Merge branch 'eventsheet-development' 2024-05-24 10:42:06 -05:00
a06807b028 fixed adjacent lineup 2024-05-23 08:03:30 -05:00
07be459781 cleanup some styling 2024-05-22 16:47:25 -05:00
769fa60196 incorporating changes for updated svg 2024-05-22 16:09:33 -05:00
d377399c10 incorporating changes for updated svg 2024-05-22 16:07:08 -05:00
fa50ab93dc incorporate letter sized sheet
accessible when url parameter ?sheet_size=letter
2024-05-22 13:41:17 -05:00
73e3afcecc Merge branch 'main' into docker 2024-05-20 09:11:25 -05:00
af9fa3bd9b fix for if availabilities aren't available 2024-05-20 09:11:08 -05:00
7e803cc8e3 fix for if availabilities aren't available 2024-05-20 09:07:54 -05:00
339d4c7923 Merge branch 'duplicate-checking' 2024-05-20 09:07:30 -05:00
58825b5bcd reloads the page on lineup save
realized that the duplicates come from saving the lineup more than once. this occurs when initially no one has an eventlineupentry, it gets one on save, but the front end is not updated on save, so it keeps creating entrires on save. simple fix is to refresh the page on save. "better" solution would be to have the front end update on save, but that's longer to implement. i started this by having `postEventLineup` return the newly fetched lineupentries.
2024-05-20 09:06:53 -05:00
6b9e6734fe Merge branch 'main' into docker 2024-05-19 12:35:31 -05:00
03a9ac3aae Merge branch 'eventsheet-development' 2024-05-19 12:35:14 -05:00
da159f2b13 urlencode needs an explicit extended option 2024-05-19 12:34:27 -05:00
c2c192bdc6 add blank lineupsheet 2024-05-19 08:53:26 -05:00
c2b1898b91 styling updates 2024-05-19 08:52:48 -05:00
4ee466e7cb Merge branch 'main' into eventsheet-development 2024-05-14 19:09:33 -05:00
6592f2eeae Merge branch 'main' into docker 2024-05-10 17:55:15 -05:00
1f2ba45e54 bug fix for initSlots 2024-05-10 17:55:03 -05:00
588c23ec3f refactoring eventsheet 2024-05-10 17:54:45 -05:00
1c3cafdcda Merge branch 'main' into docker 2024-05-06 16:51:00 -05:00
fb0ca76c29 reimplement eventlineup slots, eventsheet refinements
fixed the loop in eventlineup slots so they will show duplicate eventlineupentries if they exist
refined some styling in eventsheet
2024-05-06 16:49:21 -05:00
bdb6a77371 begin adding availability reminders 2024-05-06 16:46:29 -05:00
e4b981d676 reimplement eventlineup slots, eventsheet refinements
fixed the loop in eventlineup slots so they will show duplicate eventlineupentries if they exist

refined some styling in eventsheet
2024-05-06 16:45:56 -05:00
c4c0d0fb7d move eventlineup helpers to separate file 2024-05-06 10:41:44 -05:00
3695cd8975 fix for grouping items with no type
Fixed an error that caused the app to crash when trying to group a bulk set of items when there are items with no "type" property. Items something like "delete request" items had no type.
2024-05-06 10:40:15 -05:00
bcade85182 eventsheet styling improvements 2024-05-06 10:38:52 -05:00
d50f94acc8 update better-sqlite
needed to update better-sqlite to work with node 22 (i think? this fixed it anyways)
2024-05-06 10:36:45 -05:00
5e1facf24a Merge branch 'main' into docker 2024-05-03 07:47:05 -05:00
cf01bf9fff styling fixes
Fixes for event sheet, added availability bar to event lineup
2024-05-03 07:46:05 -05:00
83e722cdb9 add availability bar to lineup view 2024-05-03 07:41:54 -05:00
dd48aeca8d eventsheet styling fixes 2024-05-03 07:41:29 -05:00
f421089eb9 Merge branch 'main' into docker 2024-03-22 16:05:34 -05:00
d484f8cfdf csrf fix when added lineup 2024-03-22 16:03:57 -05:00
87735f76b5 fix submit to actual submitted lineup 2024-03-22 16:03:46 -05:00
6d4600a858 fix submit animation when there's more than one lineup 2024-03-22 16:03:19 -05:00
5c19a16f8b flags fix 2024-03-22 15:50:33 -05:00
aea6a64b69 add session expiry date (24 hours) 2024-03-22 15:49:52 -05:00
b9ead6770a styling changes 2024-03-22 15:49:17 -05:00
84cc1f651c Merge branch 'main' into docker 2024-03-18 20:22:15 -05:00
a1cb6fcf0a eventlineup enhancments (availability notes, reminders, flags popup) 2024-03-18 20:20:00 -05:00
e459a0688a re-add padding on main content in layout 2024-03-18 20:19:08 -05:00
8e98286f7c eventsheet styling fixes 2024-03-18 20:18:33 -05:00
87fb835590 gamesheet hide logo if not exists 2024-03-18 20:17:59 -05:00
1f897d2b1e styling fix on game sheet (front cover) 2024-03-18 20:17:20 -05:00
8c1b325532 hide "members" section for now, not implemented yet 2024-03-18 20:15:33 -05:00
4b56259b98 fix change game card -> game sheet 2024-03-18 20:14:52 -05:00
16b1402c6f re-add margin for panels of panels 2024-03-18 20:14:24 -05:00
e4f6576847 more fixes for flag set vs. array 2024-03-18 20:13:40 -05:00
2df02e7452 auth fix 2024-03-18 20:12:41 -05:00
99d376af4c re-enable 404 catchall 2024-03-18 20:12:12 -05:00
2b497a0227 fix recent, upcoming date picking 2024-03-18 20:11:52 -05:00
00b270e0f6 fix event sheet (flags using set, not array) 2024-03-18 20:10:54 -05:00
2a2eb07823 cleanup some auth stuff 2024-03-17 10:46:18 -05:00
39e6c2b5af Merge branch 'main' into docker 2024-03-15 19:14:01 -05:00
832fb654ec visual refinements for multi lineups 2024-03-15 19:09:17 -05:00
9f9da4e191 Merge branch 'main' into docker 2024-03-15 14:20:38 -05:00
f2371c6b5a add insert lineup before and after 2024-03-15 14:19:45 -05:00
dc17ca76ba add script_tags helper 2024-03-15 14:19:45 -05:00
d24b2a121e respect environment is development on error message 2024-03-15 14:19:45 -05:00
7c5630c5ba Merge branch 'main' into docker 2024-03-15 08:41:20 -05:00
e4b4345cff refinement for final score table on gamesheet card 2024-03-15 08:41:02 -05:00
dfab474f42 add manifest for pwa 2024-03-15 08:40:42 -05:00
053f6038f6 refinements 2024-03-15 08:40:27 -05:00
cb69521875 init flag checkboxes 2024-03-15 08:39:56 -05:00
58c870ce7c navbar, login refinements 2024-03-15 08:39:22 -05:00
61b6dc8a35 add dr, dh flags 2024-03-15 08:38:15 -05:00
d72ff726a5 Merge branch 'main' into docker 2024-03-12 08:37:47 -05:00
b53c8c532e remove legacy pug templates 2024-03-12 08:37:13 -05:00
4f30021a99 add logout, team selection 2024-03-12 08:36:05 -05:00
05c948fd16 add domain to caddy file via env variable 2024-03-12 08:03:42 -05:00
7a1dfa3165 Merge branch 'main' into docker 2024-03-10 15:40:13 -05:00
fda4e1c3cc fix issue with redirect to favicon on login 2024-03-10 15:39:12 -05:00
c8d0221247 dockerizing 2024-03-10 14:16:31 -05:00
a9fa89107e updates to teamsnapCallback 2024-03-10 14:01:09 -05:00
7efb083e1d update branch of teamsnap-javascript-sdk 2024-03-10 14:00:39 -05:00
fb648d477f remove errant console.log 2024-03-09 16:48:01 -06:00
6076a2801e edit teamsnap_star to mimic icons from teamsnapui 2024-03-09 16:47:03 -06:00
b2b2dba352 changes regarding error handling 2024-03-09 16:45:27 -06:00
b9f9c8455f change todays-game to defense card 2024-03-09 16:44:35 -06:00
54dac93da5 minor css changes 2024-03-09 16:43:06 -06:00
3a95ca4b74 remove border from panel on full width 2024-03-09 16:41:54 -06:00
450499d9aa widen select box 2024-03-09 16:41:27 -06:00
39d1a37043 add compose box to email 2024-03-09 16:41:08 -06:00
35d6eba599 support for flags in eventLineup edit 2024-03-05 09:15:18 -06:00
2f2f33ce74 fix for empty eventlineupentries list 2024-03-05 07:43:26 -06:00
66c18479b3 breakout components of sheet use full card defense 2024-03-05 07:42:39 -06:00
a505747b06 2023-03-04 2024-03-04 13:32:25 -06:00
6576d17539 reorganized 2023-08-19 16:30:28 -05:00
70a7981ca5 reorganize start... 2023-08-19 12:13:41 -05:00
c9eaadf688 add opponents 2023-08-17 08:25:51 -05:00
96 changed files with 15836 additions and 9151 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.env .env
var var
src/public/media/
#ide #ide
/.nova /.nova

35
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach",
"port": 9229,
"request": "attach",
"restart": true,
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "/home/node/app/src"
},
{
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"name": "nodemon (dev)",
"program": "dev",
"request": "launch",
"restart": true,
"runtimeExecutable": "nodemon",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"env": {"NODE_ENV": "development", "DEBUG": "app"},
"preLaunchTask": "npm: build-css"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
}

23
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch-scss",
"problemMatcher": [],
"label": "npm: watch-scss",
"detail": "npm run watch-css",
"icon": {
"id": "eye",
"color": "terminal.ansiBlue"
}
},
{
"type": "npm",
"script": "build-css",
"problemMatcher": [],
"label": "npm: build-css",
"detail": "npm build-css"
}
]
}

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:21
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
RUN mkdir -p /home/node/app/var/db && chown -R node:node /home/node/app
WORKDIR /home/node/app
USER node
COPY --chown=node:node package*.json ./
RUN npm install
COPY --chown=node:node src src
COPY --chown=node:node bin bin
EXPOSE 3000
CMD [ "npm", "start" ]

86
app.js
View File

@@ -1,86 +0,0 @@
require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var session = require("express-session");
var csrf = require("csurf");
var passport = require("passport");
var logger = require("morgan");
global.XMLHttpRequest = require("xhr2");
var teamsnap = require("teamsnap.js");
var indexRouter = require("./routes/index");
var authRouter = require("./routes/auth");
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.locals.pluralize = require("pluralize");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(
"/css",
express.static(path.join(__dirname, "node_modules/bootstrap/dist/css"))
);
app.use(
"/css",
express.static(
path.join(__dirname, "node_modules/@teamsnap/teamsnap-ui/dist/css")
)
);
app.use(
"/font",
express.static(path.join(__dirname, "node_modules/bootstrap-icons/font"))
);
app.use(
session({
teamsnap_token: "",
current_team: "",
secret: "keyboard cat",
resave: false, // don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
})
);
app.use(csrf());
app.use(passport.authenticate("session"));
app.use(function (req, res, next) {
var msgs = req.session.messages || [];
res.locals.messages = msgs;
res.locals.hasMessages = !!msgs.length;
req.session.messages = [];
next();
});
app.use(function (req, res, next) {
res.locals.csrfToken = req.csrfToken();
next();
});
app.use("/", authRouter);
app.use("/", indexRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
console.log("error:", err);
// render the error page
res.status(err.status || 500);
res.render("error", { message: err.message });
});
module.exports = app;

23
bin/www
View File

@@ -4,11 +4,13 @@
* Module dependencies. * Module dependencies.
*/ */
var app = require("../app"); var {app} = require("../src/app");
var http = require("http"); var http = require("http");
var https = require("https"); var https = require("https");
var fs = require("fs"); var fs = require("fs");
var debug = require("debug")("https"); var debug = require("debug")("https");
const path = require("path");
var livereload = require("livereload");
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
@@ -16,16 +18,17 @@ var debug = require("debug")("https");
var port = normalizePort(process.env.PORT || "3000"); var port = normalizePort(process.env.PORT || "3000");
app.set("port", port); app.set("port", port);
var server = http.createServer(app);
/** if (process.env.NODE_ENV === "development") {
* Create HTTPS server. const liveReloadServer = livereload.createServer({port:35729});
*/ liveReloadServer.watch(path.join(__dirname, "../src/views"));
const https_options = { liveReloadServer.server.once("connection", () => {
key: fs.readFileSync("certs/key.pem"), setTimeout(() => {
cert: fs.readFileSync("certs/cert.pem"), liveReloadServer.refresh("/");
}; }, 100);
});
var server = https.createServer(https_options, app); }
/** /**
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces.

26
caddy/Caddyfile Normal file
View File

@@ -0,0 +1,26 @@
{
{$LOG_LEVEL} # Set via environment variable
}
localhost {
# Development configuration
@notProd {
expression {env.ENVIRONMENT} == 'development'
}
handle @notProd {
# Configuration that only applies when not in production
reverse_proxy app-dev:3000
}
}
{$DOMAIN} {
# Production configuration
@prod {
expression {env.ENVIRONMENT} == 'production'
}
handle @prod {
# Configuration that only applies in production
# header Strict-Transport-Security "max-age=31536000;"
reverse_proxy app:3000
}
}

50
docker-compose.yml Normal file
View File

@@ -0,0 +1,50 @@
services:
app: &app
env_file:
- .env
build: .
networks:
- web
profiles:
- production
expose:
- 3000
app-dev:
<<: *app
ports:
- 9229:9229 #debugger
- 35729:35729 #livereload
profiles:
- development
command: npm run dev
volumes:
- ./src:/home/node/app/src
- ./package.json:/home/node/app/package.json
- ./package-lock.json:/home/node/app/package-lock.json
- ./certs:/home/node/app/certs
- ./bin/www:/home/node/app/bin/www
environment:
DEBUG: "app"
NODE_ENV: "development"
caddy:
image: caddy
ports:
- 80:80
- 443:443
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
env_file:
- .env
networks:
web:
volumes:
caddy_data:
caddy_config:

2226
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,28 +23,53 @@
"url": "https://github.com/sponsors/anthonyscorrea" "url": "https://github.com/sponsors/anthonyscorrea"
}, },
"scripts": { "scripts": {
"start": "node ./bin/www" "start": "node ./bin/www",
"dev": "nodemon --inspect=0.0.0.0 ./bin/www",
"build-css": "sass src/scss:src/public/css",
"watch-scss": "nodemon -e scss -x \"npm run build-css\""
},
"nodemonConfig": {
"ext": "js,hbs,scss",
"watch": ["src"]
}, },
"dependencies": { "dependencies": {
"@teamsnap/teamsnap-ui": "^3.12.3", "@teamsnap/teamsnap-ui": "^3.12.3",
"better-sqlite3": "^9.6.0",
"better-sqlite3-session-store": "^0.1.0",
"bootstrap": "^5.3.1", "bootstrap": "^5.3.1",
"bootstrap-icons": "^1.10.5", "bootstrap-icons": "^1.10.5",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"csurf": "^1.11.0", "cors": "^2.8.5",
"cors-anywhere": "^0.4.4",
"csrf-csrf": "^3.0.3",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^8.6.0", "dotenv": "^8.6.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"handlebars-dateformat": "^1.1.3",
"hbs": "^4.2.0",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"multer": "^1.4.5-lts.1",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-teamsnap": "^1.1.1", "passport-teamsnap": "^1.1.1",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"teamsnap.js": "^1.62.1", "sass": "^1.77.2",
"sortablejs": "^1.15.0",
"teamsnap.js": "github:anthonyscorrea/teamsnap-javascript-sdk#link-with-null-link",
"tinymce": "^6.8.3",
"underscore": "^1.13.6",
"xhr2": "^0.2.1" "xhr2": "^0.2.1"
},
"devDependencies": {
"connect-livereload": "^0.6.1",
"install": "^0.13.0",
"livereload": "^0.9.3",
"nodemon": "^3.0.3",
"pug2hbs": "^1.0.5"
} }
} }

View File

@@ -1,661 +0,0 @@
@import url("../css/paper.css");
@import url("../fonts/vera/bitstreamvera.css");
@import url("../fonts/verdana/verdanapro.css");
@import url("../fonts/m+1m/m+1m.css");
@import url("../fonts/helvetica-now/stylesheet.css");
@import url("../fonts/futura-now/stylesheet.css");
@import url("../fonts/inconsolata/stylesheet.css");
:root {
--color-success: #b7e1cd;
--color-danger: #f4c7c3;
--color-neutral: #acc9fe;
--color-warning: rgb(249, 228, 180);
--color-grey-100: #f8f9fa;
--color-grey-200: #e9ecef;
--color-grey-300: #dee2e6;
--color-grey-400: #ced4da;
--color-grey-500: #adb5bd;
--color-grey-600: #6c757d;
--color-grey-700: #495057;
--color-grey-800: #343a40;
--color-grey-900: #212529;
--row-height: 14px;
--monospace-font: "Inconsolata";
}
body {
font-family: "Helvetica Now";
position: relative;
font-size: 11px;
}
table {
position: inherit;
font-size: inherit;
border-collapse: collapse;
empty-cells: show;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
border: 0.5px solid black;
display: inline-table;
}
.bar-right {
float: right;
}
.bar-left {
float: left;
}
th,
td {
/* vertical-align: middle; */
/* line-height: 1.3em; */
overflow: hidden;
padding: 0 2px 0 2px; /* top right bottom left */
}
tr {
border-bottom-width: 0.5px;
border-color: grey;
border-bottom-style: solid;
}
tr:last-child {
border-bottom-color: black;
}
tr:first-child {
border-top-color: black;
}
tr:nth-child(odd) {
background-color: rgb(242, 242, 242, 0.85);
}
tr:nth-child(even) {
background-color: rgb(256, 256, 256, 0.85);
}
td:not(:first-child) {
border-left: 0.5px solid grey;
}
th {
font-stretch: extra-condensed;
width: 1em;
text-align: center;
text-transform: uppercase;
}
td:empty::after,
th:empty::after {
content: "\00a0";
}
td.player-name {
text-transform: uppercase;
font-stretch: 80%;
/* font-family: var(--monospace-font); */
}
td.position,
td.jersey-number {
font-family: var(--monospace-font);
width: 2ch;
text-align: right;
overflow: hidden;
}
.index-card .gamecard {
position: relative;
}
.B5 > .gamecard {
/* height: auto; */
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
/* padding: 0.15in; */
/* grid-gap: 0.25in 0.15in; */
/* background-image: url("../2023-G08-0523.png"); */
background-size: 100%;
}
.gamecard > section {
/* margin: 0.07in; */
box-sizing: content-box;
/* border: 4px solid var(--color-grey-200); */
/* border-radius: 4px; */
overflow: hidden;
outline: 0.5px dashed lightgrey;
/* border-right: 1px dotted black; */
display: flex;
align-items: stretch;
}
.gamecard > section > div {
margin: 0.15in;
outline: 0.5px solid black;
display: flex;
flex: 1; /* consumes all free space (taking full height) */
align-items: stretch;
height: auto;
}
#lineup-card-dugout div.grid-container,
#lineup-card-dugout-empty div.grid-container {
display: grid;
grid-template-columns: 60% auto;
grid-template-rows: fit-content(16px) auto;
grid-template-areas:
"header header"
"sarting-lineup-table substitution-table";
}
#lineup-card-exchange div.grid-container,
#lineup-card-exchange-empty div.grid-container {
display: grid;
grid-template-columns: auto;
grid-template-rows: fit-content(16px) auto;
grid-template-areas:
"header"
"sarting-lineup-table";
}
.lineup-card div.grid-container > .section-header {
grid-area: "header";
font-size: 14px;
font-weight: bold;
}
.lineup-card div.grid-container > .section-header:empty::after {
content: "\00a0";
}
div.grid-container > .starting-lineup-table {
grid-area: "starting-lineup-table";
}
div.grid-container > .substitution-table {
grid-area: "substitution-table";
}
.lineup-card thead th {
color: var(--color-grey-600);
font-size: 0.7em;
}
.lineup-card th.sequence {
color: var(--color-grey-600);
font-size: inherit;
}
.lineup-card table {
font-size: 22px;
}
.lineup-card td {
height: 33.5px;
}
#lineup-card-exchange tr,
#lineup-card-exchange-empty tr,
#lineup-card-dugout .starting-lineup-table tr,
#lineup-card-dugout .substitution-table tr:nth-child(odd) {
border-top: 1px solid black;
}
#lineup-card-exchange tr,
#lineup-card-exchange-empty tr,
#lineup-card-dugout .starting-lineup-table tr,
#lineup-card-dugout .substitution-table tr:nth-child(even) {
border-bottom: 1px solid black;
}
#lineup-card-exchange td.player-name {
font-stretch: 100%;
}
#lineup-card-dugout td.player-name {
width: 10ch;
}
#lineup-card-dugout td.substitution,
#lineup-card-dugout-empty td.substitution {
font-size: 11px;
height: 15px;
}
.lineup-card .position,
.lineup-card .jersey-number {
width: 2ch;
}
#lineup-card-dugout .position,
#lineup-card-dugout .jersey-number {
font-stretch: 75%;
padding-left: 2.5px;
padding-right: 2.5px;
}
.lineup-card .section-header {
padding-left: 1px;
padding-right: 1px;
font-size: inherit;
text-transform: uppercase;
font-stretch: 85%;
}
#todays-game > div {
display: grid;
/* gap: 0.5px; */
grid-template-columns: 110px auto;
grid-template-rows: calc(var(--row-height) * 1) auto auto;
grid-template-areas:
"header header"
"offense defense"
"footer footer";
}
#defense-pane {
position: relative;
/* box-sizing: border-box; */
padding: 4px 4px 0px 4px; /* top right bottom left */
display: flex;
grid-area: defense;
border-left: 0.5px solid black;
border-bottom: 0.5px solid black;
}
#offense-pane {
position: relative;
/* box-sizing: border-box; */
height: 100%;
grid-area: offense;
border-bottom: 0.5px solid black;
/* outline: 0.5px solid black; */
}
#offense-pane table {
height: 100%;
border: none;
}
#defense-pane .container {
width: 100%;
display: grid;
grid-template-rows: auto;
}
#defense-pane .pitching-container {
margin: auto 0 0 0; /* top right bottom left */
}
#defense-pane .field-container {
display: grid;
grid-template-rows: auto;
/* margin: top; */
width: 100%;
/* background: url("../baseball-diamond.svg"); */
background-size: 100%;
background-position: center 10px;
background-repeat: no-repeat;
gap: 6px;
z-index: 2;
}
#defense-pane img {
position: absolute;
z-index: -1;
}
.defense-slot-set {
width: 77px;
}
.index-card .defense-slot-set {
width: 65px;
}
.index-card .defense-slot-set .player-name {
font-stretch: 70%;
}
.pitching-container .defense-slot-set {
width: 100%;
}
.container .row {
width: 100%;
display: flex;
align-items: center;
}
.section-header {
background-color: #cadcf9;
font-size: 8.8px;
/* outline: 1px solid black; */
/* height: var(--row-height); */
width: auto;
grid-area: header;
text-align: center;
padding-left: 10px;
padding-right: 10px;
border-bottom: 0.5px solid black;
z-index: 1;
}
.footer {
/* height:var(--row-height); */
position: relative;
box-sizing: border-box;
grid-area: footer;
/* border: 1px solid black; */
height: 100%;
}
.footer table {
height: 100%;
outline: none;
border-style: none;
}
.footer tr {
background-color: white;
outline: none;
border-bottom: 0.5px solid var(--color-grey-500);
}
.footer tr :last-child {
background-color: white;
outline: none;
border-bottom-style: none;
}
.footer th {
text-align: left;
color: var(--color-grey-600);
}
.footer td {
height: var(--row-height);
border: none;
}
.footer td:empty::after {
content: "";
}
.cell-checkbox {
font-size: 0.75em;
}
.in-starting-lineup {
font-weight: bold;
}
.gametitle {
font-weight: bold;
text-transform: uppercase;
font-stretch: semi-condensed;
}
.homeaway {
text-transform: uppercase;
font-stretch: normal;
font-weight: bolder;
float: right;
text-transform: uppercase;
}
.cell-smalltext {
font-stretch: condensed;
font-size: 10px;
}
.statscell {
font-family: "m+1m";
text-align: center;
font-stretch: extra-condensed;
font-size: 9px;
width: 60px;
}
{
text-transform: uppercase;
}
.condensedNameCell {
width: 70px;
text-transform: uppercase;
font-stretch: condensed;
}
.cell-square {
height: var(--row-height);
width: 14px;
text-align: center;
}
.cell-square.narrow {
width: 10px;
}
.cell-mono {
font-family: "m+1m";
}
.cell-condensed {
font-stretch: condensed;
}
.available-status-code-1 {
color: rgb(0, 85, 0);
background-color: #b7e1cd;
}
.available-status-code-0 {
color: rgb(170, 0, 0);
background-color: #f4c7c3;
}
.past.available-status-code-0,
.past.available-status-code-null {
color: var(--color-grey-600);
background-color: inherit;
}
.past.available-status-code-1 {
color: inherit;
background-color: var(--color-warning);
}
.past.available-status-code-1.started {
color: inherit;
background-color: inherit;
}
.available-status-code-2 {
color: blue;
background-color: #acc9fe;
}
#roster-and-history .player-name,
#roster-and-history .jersey-number {
color: black;
}
#roster-and-history .player-name {
font-stretch: normal;
}
.starting {
font-weight: bold;
}
#roster-and-history > div > table {
/* font-size: 10.5px; */
padding: 0;
line-height: 1em;
/* outline: 0.5px black; */
}
#roster-and-history td,
#roster-and-history th {
border-left: none;
border-right: none;
padding: 0.2em 0.1em 0.2em 0.1em; /* top right bottom left */
}
#roster-and-history td.player-name {
text-align: left;
}
#roster-and-history th {
background-color: #cadcf9;
color: black;
border: none;
}
#roster-and-history thead > tr,
#roster-and-history tfoot > tr {
border-bottom: solid black 1px;
}
#roster-and-history tbody {
border-bottom: solid black 1px;
}
#roster-and-history td[id^="avail"][id$="today-plus-1"],
#roster-and-history .pitcher,
#roster-and-history .player-stats,
#roster-and-history td[id^="avail"][id$="today-minus-1"] {
border-left-width: 1px;
border-left-style: solid;
border-left-color: black;
}
#roster-and-history td.jersey-number {
border-left: 0.5px solid lightgrey;
}
#roster-and-history td.today-minus-4 {
border-right: 1px solid black;
}
#roster-and-history tr.border-top {
border-top: 1px solid black;
}
#roster-and-history #today-availability {
font-stretch: normal;
text-transform: uppercase;
font-size: 0.8em;
}
.player-stats {
font-family: var(--monospace-font);
font-size: 1em;
font-stretch: 60%;
font-weight: 300;
}
#roster-and-history td.position-capability,
th.position-capability {
font-size: 8px;
font-stretch: 50%;
width: 5px;
text-align: center;
padding: 0;
}
#roster-and-history th.position-capability {
font-size: inherit;
}
td.position-capability:not(:empty) {
color: var(--color-grey-700);
background-color: var(--color-grey-200);
}
td.is-present-checkbox {
font-size: 0.5em;
text-align: center;
color: white;
/* text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000; */
}
td.is-present-checkbox.available-status-code-0 > span {
display: none;
}
td.is-present-checkbox.available-status-code-None > span {
display: none;
}
td.availability {
font-family: var(--monospace-font);
font-stretch: condensed;
text-align: center;
max-width: 0.8em;
min-width: 0.8em;
}
.availability.future,
.availability.past {
font-family: var(--monospace-font);
font-stretch: condensed;
font-weight: normal;
font-size: 0.8em;
padding: 0.1em;
text-transform: uppercase;
}
#roster-test .player-name {
font-weight: bold;
text-transform: uppercase;
grid-area: player-name;
}
#roster-test .jersey-number {
font-weight: bolder;
font-stretch: extra-condensed;
grid-area: jersey-number;
}
#roster-test .player-stats {
grid-area: player-stats;
}
#roster-test .diamond {
grid-area: diamond;
overflow: hidden;
display: inline-block;
height: 10px;
/* height:10px; */
}
.rotate {
transform: rotate(270deg);
}
.delimiter,
.decimal-point {
font-family: Helvetica Now;
font-stretch: expanded;
color: var(--color-grey-500);
}
th .decimal-point {
color: rgb(0, 0, 0, 0);
}
th .delimiter {
color: var(--color-grey-500);
}

View File

@@ -1,115 +0,0 @@
event_id = "292333461";
event_id_2 = "292333462";
team_id = "6882652";
function format_stat(number) {
const zeroPad = (num, places) => String(num).padStart(3, "0");
return zeroPad(Math.round(number * 1000), 3);
}
async function load_data_xxx() {
const event_id = document.querySelector('input[name="event_id"]').value;
const team_id = document.querySelector('input[name="team_id"]').value;
update_card(team_id, event_id);
}
async function update_card(team_id, event_id) {
fetch(`/${team_id}/event/${event_id}/gamecard/data`, {
method: "GET",
headers: {
Accept: "application/json",
},
})
.then((response) => response.json())
.then(function (items) {
console.log(items);
events = items.filter(function (item) {
return item.type == "event";
});
event_index = events.findIndex(function (e) {
return e.id == event_id;
});
event = events[event_index];
document.title = event.formattedTitle;
document.querySelectorAll(".event-title").forEach(function (element) {
element.innerText = event.formattedTitle;
});
document.querySelectorAll(".event-label").forEach(function (element) {
element.innerText = event.label;
});
document
.querySelectorAll(".event-location-name")
.forEach(function (element) {
element.innerText = event.locationName;
});
document.querySelectorAll(".opponent").forEach(function (element) {
element.innerText = event.opponentName;
});
document.querySelectorAll(".homeaway").forEach(function (element) {
element.innerText = event.gameType;
});
document.querySelectorAll(".event-date").forEach(function (element) {
element.innerText = new Date(event.startDate).toLocaleDateString(
"en-us",
{
weekday: "short",
day: "numeric",
// year: "numeric",
month: "short",
}
);
});
document.querySelectorAll(".event-time").forEach(function (element) {
element.innerText = new Date(event.startDate).toLocaleTimeString(
"en-us",
{
hour: "numeric",
minute: "2-digit",
}
);
});
document.getElementById("todays-game-header").innerText =
event.formattedTitle +
" - " +
new Date(event.startDate).toLocaleDateString("en-us", {
weekday: "short",
day: "numeric",
// year: "numeric",
month: "short",
}) +
" " +
new Date(event.startDate).toLocaleTimeString("en-us", {
hour: "numeric",
minute: "2-digit",
});
for (let j = -4; j < 5; j++) {
if (j < 0) {
plus_minus = "minus";
} else if (j > 0) {
plus_minus = "plus";
} else {
continue;
}
document.querySelector(
`th.today-${plus_minus}-${Math.abs(j)} div`
).textContent = new Date(
events[event_index + j].startDate
).toLocaleDateString("en-us", {
weekday: "short",
});
}
console.log({
0: events[event_index],
1: events[event_index + 1],
});
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 198 371" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1.5;">
<g transform="matrix(1.04762,0,0,1.04802,-58.5948,32.9153)">
<path d="M90,150L60,119.396C60,119.396 77.426,47.232 150,47.383C220.563,47.53 240.863,120.923 240.863,120.923L210,150" style="fill:none;stroke:rgb(13,202,242);stroke-opacity:0.42;stroke-width:4.17px;"/>
<g transform="matrix(1.97279,0,0,1.79916,-103.351,-30.936)">
<path d="M128.423,67.218L158.837,100.567L128.423,133.916L98.009,100.567L128.423,67.218Z" style="fill:rgb(13,202,242);fill-opacity:0.1;stroke:rgb(13,202,242);stroke-opacity:0.42;stroke-width:2.21px;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,390 +0,0 @@
var express = require("express");
var ensureLogIn = require("connect-ensure-login").ensureLoggedIn;
var papaparse = require("papaparse");
var ensureLoggedIn = ensureLogIn();
var router = express.Router();
function authTeamsnap(user) {
if (!teamsnap.isAuthed()) {
teamsnap.init(process.env["TEAMSNAP_CLIENT_ID"]);
teamsnap.auth(user.accessToken);
}
}
function availabilitiesSort(a, b) {
status_code_sort = [
teamsnap.AVAILABILITIES.YES,
teamsnap.AVAILABILITIES.MAYBE,
teamsnap.AVAILABILITIES.NO,
teamsnap.AVAILABILITIES.NONE,
];
a_sort = status_code_sort.indexOf(a.statusCode);
b_sort = status_code_sort.indexOf(b.statusCode);
if (a_sort > b_sort) {
return 1;
}
if (a_sort < b_sort) {
return -1;
}
if (a_sort == b_sort) {
if (a.member.lastName < b.member.lastName) {
return -1;
}
if (a.member.lastName > b.member.lastName) {
return 1;
}
}
}
async function fetch_stats(resolve, reject) {
url =
"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv&gid={tab_id}";
papaparse.Papa.parse(url, {
download: true,
complete: function (results) {
results.data.forEach((row, i) => {
if (i == 0 || row[2] == "Totals" || row[2] == "") {
return;
}
d = {
first_name: row[3],
last_name: row[2],
jersey_number: row[1],
pa: row[5],
ab: row[6],
avg: row[20],
obp: row[21],
slg: row[22],
};
});
resolve(d);
},
});
}
/* GET home page. */
router.get("/", ensureLoggedIn, function (req, res, next) {
if (req.user) {
authTeamsnap(req.user);
teamsnap.loadCollections(function (err) {
if (err) {
alert("Error loading TeamSnap SDK");
return;
}
teamsnap.loadTeams(function onTeamsLoad(err, teams) {
teams = teams.sort((a, b) => b.seasonName - a.seasonName);
res.render("home", { req: req, teams: teams });
});
});
} else {
res.render("home", { req: req });
}
});
router.get(
"/teams",
ensureLoggedIn,
function (req, res, next) {
console.log("teamsnap authed?: ", teamsnap.isAuthed());
console.log("user is", req.user);
authTeamsnap(req.user);
teamsnap.loadCollections(function (err) {
if (err) {
alert("Error loading TeamSnap SDK");
return;
}
teamsnap.loadTeams(function onTeamsLoad(err, teams) {
teams = teams.sort((a, b) => b.seasonName - a.seasonName);
res.render("teams", { teams: teams });
});
});
next();
},
function (req, res, next) {
// res.send(`${me.firstName} ${me.lastName}`);
}
);
router.get("/:team_id([0-9]+)", ensureLoggedIn, function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
console.log("team_id", team_id);
teamsnap.loadCollections(function (err) {
if (err) {
alert("Error loading TeamSnap SDK");
return;
}
teamsnap.enablePersistence();
teamsnap.bulkLoad(
team_id,
["team", "member", "event", "opponent", "availability_summary"],
function onBulkLoad(err, items) {
team = items.find((i) => (i.type == "team") & (i.id == team_id));
console.log(team);
res.set("Content-Type", "text/html");
res.render("team", { team: team });
}
);
});
});
router.get(
"/:team_id/event/:event_id",
ensureLoggedIn,
function (req, res, next) {
authTeamsnap(req.user);
var team_id = req.params.team_id;
var event_id = req.params.event_id;
teamsnap.loadCollections(function (err) {
console.log();
teamsnap.enablePersistence();
teamsnap.bulkLoad(
team_id,
["team", "event", "availabilitySummary"],
function (err, items) {
if (err) {
res.code = 500;
res.send(err);
}
availabilitySummaries = items.filter(
(i) => i.type == "availabilitySummary" && i.id == event_id
);
events = items.filter((i) => i.type == "event" && i.id == event_id);
if (events) {
event = events[0];
availabilitySummary = availabilitySummaries[0];
console.log("A_S", availabilitySummaries);
res.render("event", {
event: event,
team_id: team_id,
team: items.find((i) => i.type == "team" && i.id == team_id),
availabilitySummary: availabilitySummary,
});
} else {
res.code = 500;
res.send("error");
}
}
);
});
}
);
router.get(
"/:team_id/event/:event_id/gamecard",
ensureLoggedIn,
function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
event_id = req.params.event_id;
teamsnap.loadCollections((err) => {
teamsnap.enablePersistence();
var events;
teamsnap
.bulkLoad(team_id, [
"team",
"member",
// "member_photos",
"event",
"opponent",
"availability_summary",
])
.then((items) => {
events = items
.filter((i) => i.type == "event")
.sort((a, b) => a.startDate - b.startDate);
event = events.find((i) => i.id == event_id);
events_past = events.slice(
events.findIndex((e) => e == event) - 4,
events.findIndex((e) => e == event)
);
events_future = events.slice(
events.findIndex((e) => e == event) + 1,
events.findIndex((e) => e == event) + 5
);
events = events_past.concat(event).concat(events_future);
})
.then((items) => {
return teamsnap.loadAvailabilities({
eventId: events.map((e) => e.id),
});
})
.then(() => {
return teamsnap.collections["eventLineups"]
.queryItems("search", {
eventId: events.map((e) => e.id),
})
.then((event_lineups) => {
return Promise.all(
event_lineups.map((elu) => elu.loadItem("eventLineupEntries"))
);
});
})
.then(() => {
items = teamsnap.getAllItems();
events = items.filter((i) => i.type == "event");
current_event_index = events.findIndex((e) => e.id == event_id);
context = {
team_id: req.params.team_id,
event_id: req.params.event_id,
current_event_index: current_event_index,
events: items.filter((a) => a.type == "event"),
availabilitySummaries: items.filter(
(i) => i.type == "availabilitySummary"
),
event: items.find((e) => e.type == "event" && e.id == event_id),
events_past: events_past,
events_future: events_future,
members: items.filter((a) => a.type == "member"),
availabilities: items
.filter((i) => i.type == "availability")
.sort(availabilitiesSort),
all_lineup_entries: items.filter(
(i) => i.type == "eventLineupEntry"
),
event_lineup_entries_offense: items
.filter(
(i) =>
i.type == "eventLineupEntry" &&
i.eventId == event_id &&
!i.label.includes("[PO]")
)
.sort((a, b) => a.sequence - b.sequence),
event_lineup_entries: items
.filter(
(i) => i.type == "eventLineupEntry" && i.eventId == event_id
)
.sort((a, b) => a.sequence - b.sequence),
};
res.render("gamecard", context);
});
});
}
);
router.get(
"/:team_id/event/:event_id/lineup",
ensureLoggedIn,
function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
event_id = req.params.event_id;
teamsnap.loadCollections((err) => {
teamsnap.enablePersistence();
var events;
teamsnap
.bulkLoad(team_id, [
"team",
"member",
// "member_photos",
"event",
"opponent",
"availability_summary",
])
.then((items) => {
events = items
.filter((i) => i.type == "event")
.sort((a, b) => a.startDate - b.startDate);
event = events.find((i) => i.id == event_id);
events_past = events.slice(
events.findIndex((e) => e == event) - 4,
events.findIndex((e) => e == event)
);
events_future = events.slice(
events.findIndex((e) => e == event) + 1,
events.findIndex((e) => e == event) + 5
);
events = events_past.concat(event).concat(events_future);
})
.then((items) => {
return teamsnap.loadAvailabilities({
eventId: events.map((e) => e.id),
});
})
.then(() => {
return teamsnap.collections["eventLineups"]
.queryItems("search", {
eventId: events.map((e) => e.id),
})
.then((event_lineups) => {
return Promise.all(
event_lineups.map((elu) => elu.loadItem("eventLineupEntries"))
);
});
})
.then(() => {
items = teamsnap.getAllItems();
events = items.filter((i) => i.type == "event");
current_event_index = events.findIndex((e) => e.id == event_id);
context = {
team_id: req.params.team_id,
event_id: req.params.event_id,
current_event_index: current_event_index,
events: items.filter((a) => a.type == "event"),
availabilitySummaries: items.filter(
(i) => i.type == "availabilitySummary"
),
event: items.find((e) => e.type == "event" && e.id == event_id),
events_past: events_past,
events_future: events_future,
members: items.filter((a) => a.type == "member"),
availabilities: items
.filter((i) => i.type == "availability")
.sort(availabilitiesSort),
all_lineup_entries: items.filter(
(i) => i.type == "eventLineupEntry"
),
event_lineup_entries_offense: items
.filter(
(i) =>
i.type == "eventLineupEntry" &&
i.eventId == event_id &&
!i.label.includes("[PO]")
)
.sort((a, b) => a.sequence - b.sequence),
event_lineup_entries: items
.filter(
(i) => i.type == "eventLineupEntry" && i.eventId == event_id
)
.sort((a, b) => a.sequence - b.sequence),
};
res.render("lineup", context);
});
});
}
);
router.get("/:team_id/events", ensureLoggedIn, function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
event_id = req.params.event_id;
teamsnap.loadCollections(function (err) {
teamsnap
.bulkLoad(team_id, ["team", "event", "availability_summary"])
.then((items) => {
res.set("Content-Type", "text/html");
res.render("events", {
team: items.find((i) => i.type == "team" && i.id == team_id),
events: items.filter((i) => i.type == "event"),
availabilitySummaries: items.filter(
(i) => i.type == "availabilitySummary"
),
team_id: team_id,
});
});
});
});
module.exports = router;

183
src/app.js Normal file
View File

@@ -0,0 +1,183 @@
require("dotenv").config();
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const SqliteStore = require("better-sqlite3-session-store")(session)
var sqlite = require("better-sqlite3");
const { generateToken } = require('./middlewares/csrf');
const passport = require("passport");
const logger = require("morgan");
const bodyParser = require("body-parser");
global.XMLHttpRequest = require("xhr2");
// const teamsnap = require("./public/js/teamsnap");
const teamsnap = require("teamsnap.js/lib/teamsnap")
// import {teamsnap} from "teamsnap.js"
const indexRouter = require("./routes/index").router;
const authRouter = require("./routes/auth").router;
var hbs = require('hbs');
const { embeddedSvgFromPath } = require("./lib/utils");
const cors = require('cors');
const corsOptions = {
origin: false
}
// teamsnap.init(process.env["TEAMSNAP_CLIENT_ID"])
// console.log(teamsnap)
// teamsnap.teamsnap.init
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
hbs.registerPartials(require("./routes/index").partials)
hbs.registerPartials(require("./controllers/event").partials)
hbs.registerPartials(require("./controllers/eventlineup").partials)
hbs.registerPartials(require("./controllers/eventsheet").partials)
hbs.registerHelper('dateFormat', require('handlebars-dateformat'));
hbs.registerHelper('section', (name, options) => {
if(!this._sections) this._sections = {};
this._sections[name] = options.fn(this);
return null;
})
hbs.registerHelper('script_tags', (scripts, options) => {
if(!scripts) {
return null;
}
var result = [];
scripts.forEach((script)=>{
result.push(`<script src="${script}"></script>`)
})
return result.join('\n');
})
hbs.registerHelper("embeddedSvgFromPath", require('./lib/utils').embeddedSvgFromPath)
hbs.registerHelper(require("./controllers/event").helpers)
hbs.registerHelper(require("./controllers/eventlineup").helpers)
hbs.registerHelper(require("./helpers/eventsheet"))
app.set("view engine", "hbs");
app.locals.pluralize = require("pluralize");
if (process.env.NODE_ENV === "development") {
console.log('adding connectLiveReload')
var connectLiveReload = require("connect-livereload");
app.use("/scss", express.static(path.join(__dirname, "scss")));
app.use(connectLiveReload({port: 35729, src:"http://localhost:35729/livereload.js?snipver=1"}));
}
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true }));
app.use(logger("dev"));
app.use(cors(corsOptions))
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(
"/css",
express.static(path.join(__dirname, "../node_modules/bootstrap/dist/css"))
);
app.use(
"/css",
express.static(
path.join(__dirname, "../node_modules/@teamsnap/teamsnap-ui/dist/css")
)
);
app.use(
"/teamsnap-ui/assets",
express.static(
path.join(__dirname, "../node_modules/@teamsnap/teamsnap-ui/src/assets")
)
);
app.use(
"/font",
express.static(path.join(__dirname, "../node_modules/bootstrap-icons/font"))
);
app.use(
"/bootstrap-icons",
express.static(path.join(__dirname, "../node_modules/bootstrap-icons/icons"))
);
app.use(
"/js",
express.static(path.join(__dirname, "../node_modules/sortablejs"))
);
app.use(
"/js",
express.static(path.join(__dirname, "../node_modules/tinymce"))
);
app.use(
"/js",
express.static(path.join(__dirname, "../node_modules/teamsnap.js/lib/"))
);
const db = new sqlite("./var/db/sessions.db");
app.use(
session({
store: new SqliteStore({
client: db,
expired: {
clear: true,
intervalMs: 900000 //ms = 15min
}
}),
cookie: { maxAge: 86400000 }, // value of maxAge is defined in milliseconds.
teamsnap_token: "",
current_team: "",
csrfToken:"",
secret: process.env['SECRET'],
resave: false, // don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
})
);
app.use(function (req, res, next) {
res.locals.csrfToken = generateToken;
next();
});
app.use(passport.authenticate("session", { failureRedirect: '/login', failureMessage: true }));
app.use(function (req, res, next) {
var msgs = req.session.messages || [];
res.locals.messages = msgs;
res.locals.hasMessages = !!msgs.length;
req.session.messages = [];
next();
});
app.use('/', require('./routes/meta').router);
app.use("/", authRouter);
app.use("/", indexRouter);
app.use(require("./routes/team").router)
app.use(require("./routes/opponent").router)
app.use(require("./routes/event").router)
app.use(require("./routes/eventlineup").router)
app.use(require("./routes/eventsheet").router)
// app.use("/", indexRouter.team_router);
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
if (err) {
res.locals.message = req.app.get("env") === "development" ? err.message : "An error has occurred";
res.locals.error = req.app.get("env") === "development" ? err : {};
if (typeof err === 'string' || err instanceof String) {
err = {
message: err
}
}
console.log("error:", err);
// render the error page
res.status(err.status || 500).render("error", { title:"Error", layout: req.layout, message: err.message });
}
else {
next();
}
});
// catch 404 and forward to error handler
app.use(function (req, res, next) {
// next(createError(404));
res.status(404).send('not found')
});
app.set('trust proxy')
module.exports = {app};

123
src/controllers/event.js Normal file
View File

@@ -0,0 +1,123 @@
tsUtils = require("../lib/utils");
const path = require('path');
const { teamsnapFailure, tsPromise, teamsnapCallback } = require("../lib/utils");
const {promisify} = require('util')
exports.helpers = {
availability_percentage: (availabilitySummary, status) => {
attribute = {
going: "playerGoingCount",
notgoing: "playerNotGoingCount",
maybe: "playerMaybeCount",
unknown: "playerUnknownCount"
}[status.toLowerCase()]
return (availabilitySummary[attribute]/availabilitySummary.team.playerMemberCount)*100.0
},
isAway: (event) => {
return event.gameType == "Away";
}
}
exports.partials = path.join(__dirname, "../views/event/partials")
exports.confirmModalAvailabilityReminders = async (req, res) => {
res.status(200).render("event/partials/modal_availability_reminders")
}
exports.getEvents = async (req, res, next) => {
const {user, team, layout} = req
const bulkLoadTypes = ["event", "availabilitySummary"]
// const tsPromiseBulkload = promisify(teamsnap.bulkLoad)
const promise = teamsnap.bulkLoad(
{teamId: team.id, types: bulkLoadTypes},
undefined,
(err,items) => {teamsnapCallback(err, items, {req, source: 'getEvents', method: 'bulkLoad'})}
)
.then(items=>tsUtils.groupTeamsnapItems(items))
.then(items=>{
items.events.forEach((event) => {
event.link('availabilitySummary', items.availabilitySummaries.find(a=>a.eventId==event.id))
}
)
req.events = items.events;
}
)
req.promises.push(promise)
all = await Promise.all(req.promises)
try {
const context = {
title: "Events",
user, team, layout,
events: req.events,
};
res.render("event/list", context);
} catch(e) {
next(e)
}
};
exports.getEvent = async (req, res, next) => {
await Promise.all(req.promises)
const {user, team, event, layout} = req
lineups = await req.event.loadItem('eventLineups')
event.link('availabilitySummary', req.availabilitySummary)
context = {
title: "Event",
user, team, event, layout,
availabilitySummary: req.availabilitySummary,
};
res.render("event/show", context);
};
exports.sendAvailabilityReminders = async (req,res,next) => {
await Promise.all(req.promises)
if (!req.body || ! (req.body.event_id && req.body.memberIds)) {
res.status(400).send('Malformed post')
}
if (req.params.event_id != req.body.eventId) {
// Load actual event. Do I want this to be an error? probably
res.status(400).send('Event ID parameter does not match the POST body')
return;
}
const {event} = req
const {eventId, memberIds} = req.body
const sendingMember = req.members.find(m=>m.userId==req.user.id)
try {
const promise = teamsnap.sendAvailabilityReminders(event, sendingMember, memberIds)
await promise
.then (res.status(200).send('OK'))
} catch (err) {
res.status(500).send()
}
return
}
exports.submitResetAvailabilities = async (req,res,next) => {
await Promise.all(req.promises)
if (!req.body || ! (req.body.event_id && req.body.memberIds)) {
res.status(400).send('Malformed post')
}
if (req.params.event_id != req.body.event_id) {
// Load actual event. Do I want this to be an error? probably
res.status(400).send('Event ID parameter does not match the POST body');
return
}
const {event_id, memberIds} = req.body
const reset_promises = []
const availabilities = await teamsnap.loadAvailabilities({eventId: event_id}, teamsnapCallback);
availabilities.filter(availability =>memberIds.includes(availability.memberId.toString())).forEach( availability => {
availability.statusCode = teamsnap.AVAILABILITIES.NONE
const promise = teamsnap.saveAvailability(availability, teamsnapCallback)
reset_promises.push(promise)
})
await Promise.all(reset_promises)
.then(res.status(200).send('OK'))
}

View File

@@ -0,0 +1,196 @@
const path = require('path')
const fs = require('fs')
const {groupTeamsnapItems, parsePositionLabel, compilePositionLabel, teamsnapCallback} = require("../lib/utils")
const tsUtils = require('../lib/utils')
const { loadEventLineupEntries } = require('teamsnap.js')
exports.partials = path.join(__dirname, "../views/eventlineup/partials")
exports.helpers = require('../helpers/eventlineup.js')
exports.getEventLineup = async (req, res)=>{
await Promise.all(req.promises)
const {user, team, members, event, layout, event_lineup, event_lineup_entries, availabilities, availabilitySummary, csrfToken} = req
attachBenchcoachPropertiesToMember(members, event_lineup_entries, availabilities)
members.sort(tsUtils.teamsnapMembersSortLineupAvailabilityLastName)
const scripts = [
"https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js",
"/js/eventlineup.js",
"/js/tinymce.min.js"
]
res.render("eventlineup/edit", {user, team, members, event, availabilities, scripts, layout, event_lineup, event_lineup_entries, availabilitySummary, csrfToken})
}
exports.getAdjacentEventLineup = async (req, res) => {
await Promise.all(req.promises)
const index = Number(req.query.index)
const {user, team, members, csrfToken} = req
let event
if (index > 0) {
event = req.upcoming_events[index-1]
}
else if (index < 0){
event = req.recent_events[Math.abs(index)-1]
} else {
throw new Error('Index must be positive or negative number')
}
if (!event) {
res.status(500).send()
return
}
const availabilitySummary = event.availabilitySummary
const event_lineup = req.timeline.event_lineups?.find(i=>i.eventId==event.id)
const event_lineup_entries = req.timeline.event_lineup_entries?.filter(i=>i.eventId==event.id)
const availabilities = req.timeline.availabilities.filter(i=>i.eventId==event.id)
attachBenchcoachPropertiesToMember(members, event_lineup_entries, availabilities)
members.sort(tsUtils.teamsnapMembersSortLineupAvailabilityLastName)
console.log()
res.render("eventlineup/edit", {user, team, members, event, layout: null, event_lineup, event_lineup_entries, availabilitySummary, availabilities, csrfToken})
}
attachBenchcoachPropertiesToMember = (members, event_lineup_entries, availabilities) => {
members.forEach((member)=> {
// I *think* this can be done with linking https://github.com/teamsnap/teamsnap-javascript-sdk/wiki/Persistence#linking
// here's an example:
// member.link('eventLineupEntry', event_lineup_entries.find(i=>i.id=members[1].id))
member.benchcoach = {}
// I don't really like this, but the member_id changes once a season is archived.
// as far as I can tell, member_name should consistently be formulated from first and last name
// perhaps could have some edge cases if first or last names change, but this *should be* exceedingly rare.
const member_name = `${member.firstName} ${member.lastName}`
const event_lineup_entry = event_lineup_entries?.find(e=> e.memberId == member.id || e.memberName == member_name)
const availability = availabilities.find(e=>e.memberId == member.id)
member.benchcoach.availability = availability
if (event_lineup_entry != null) {
// member.link('eventLineupEntry', event_lineup_entry)
member.benchcoach.eventLineupEntry = event_lineup_entry
const {positionLabelWithoutFlag, positionFlags} = parsePositionLabel(event_lineup_entry.label);
member.benchcoach.eventLineupEntry.positionLabelWithoutFlag = positionLabelWithoutFlag
member.benchcoach.eventLineupEntry.flags = positionFlags
}
else {
member.benchcoach.eventLineupEntry = null
}
}
)
}
exports.attachBenchcoachPropertiesToMember = attachBenchcoachPropertiesToMember
exports.getEventLineupEmail = async (req, res)=>{
const {body} = req
if (body.memberId == null) {res.status(400).end();return}
await Promise.all(req.promises)
const {user, team, members, event, layout, event_lineup, event_lineup_entries, availabilities, availabilitySummary} = req
const eventLineupEntries = req.event_lineup.eventLineupEntries
const {newEventLineupEntries} = processPostedEventLineupEntries(body, eventLineupEntries, event_lineup)
attachBenchcoachPropertiesToMember(members, newEventLineupEntries, availabilities)
members.sort(tsUtils.teamsnapMembersSortLineupAvailabilityLastName)
res.status(200).render("eventlineup/partials/email_modal.hbs", {layout:null, user, team, members, event, event_lineup, event_lineup_entries: newEventLineupEntries, availabilities, availabilitySummary})
}
exports.getAvailabilityRemindersModal = (req, res) => {
res.status(200).render("eventlineup/partials/availability_reminder_modal.hbs")
}
exports.getEventLineupEntries = async (req, res)=>{
const {event_lineup, event_lineup_entries} = req
res.setHeader('Content-Type', 'application/json').send(JSON.stringify(req.event_lineup_entries))
}
exports.getEventLineupEntriesData = async (req, res)=>{
const {event_lineup, event_lineup_entries} = req
res.setHeader('Content-Type', 'application/json').send(JSON.stringify(req.event_lineup_entries))
}
exports.postEventLineup = async (req,res) => {
const {body} = req
if (body.memberId == null) {res.status(400).end();return}
await Promise.all(req.promises);
const eventLineupEntries = req.event_lineup.eventLineupEntries
const {newEventLineupEntries, deleteEventLineupEntries} = processPostedEventLineupEntries(body, eventLineupEntries, req.event_lineup)
newEventLineupEntries.forEach(e=>{
teamsnap.saveEventLineupEntry(e, teamsnapCallback)
})
deleteEventLineupEntries.forEach(e=>{
teamsnap.deleteEventLineupEntry(e, teamsnapCallback)
})
const bulk_items = await teamsnap.bulkLoad(
{teamId: req.params.team_id, types: ['eventLineup', 'eventLineupEntry'], scopeTo:'event', event__id:req.params.event_id,},
null,
(err, items) => {teamsnapCallback(err, items, {req, source:"postEventLineup", method:'bulkLoad'})}
)
groupedReturnedItems = groupTeamsnapItems(bulk_items)
returnedEventLineupEntries = groupedReturnedItems.eventLineupEntries
res.status(201).end(JSON.stringify(returnedEventLineupEntries))
}
const processPostedEventLineupEntries = (body, eventLineupEntries, eventLineup) => {
const newEventLineupEntries = []
const deleteEventLineupEntries = []
body.memberId.forEach((memberId, i)=>{
const lineupEntryId = body.eventLineupEntryId[i]
const lineupEntryLabel = body.label[i]
const lineupEntrySequence = body.sequence[i]
const lineupEntryFlags = body.flags[i]
if (lineupEntryId != '' && lineupEntryLabel != '') {
// Update lineup entry
try {
const eventLineupEntry = eventLineupEntries.find((e)=>e.id==Number(lineupEntryId))
eventLineupEntry.sequence = lineupEntrySequence
eventLineupEntry.label = compilePositionLabel(lineupEntryLabel, lineupEntryFlags)
newEventLineupEntries.push(eventLineupEntry)
} catch {
console.log
}
}
else if (lineupEntryId != '') {
// Delete lineup entry
const eventLineupEntry = eventLineupEntries.find((e)=>e.id==Number(lineupEntryId))
deleteEventLineupEntries.push(eventLineupEntry)
}
else if (lineupEntryLabel != '') {
// Create lineup entry
const eventLineupEntry = teamsnap.createEventLineupEntry()
eventLineupEntry.eventLineupId = eventLineup.id
eventLineupEntry.memberId = memberId
eventLineupEntry.sequence = lineupEntrySequence
eventLineupEntry.label = compilePositionLabel(lineupEntryLabel, lineupEntryFlags)
newEventLineupEntries.push(eventLineupEntry)
}
else {
// Skip lineup entry
}
})
return {newEventLineupEntries, eventLineupEntries, deleteEventLineupEntries}
}
exports.submitDeleteEventLineupEntries = async (req,res) => {
await Promise.all(req.promises);
const {event_lineup, event_lineup_entries} = req
let event_id
let memberIds
if (!req.body || ! (req.body.event_id && req.body.memberIds)) {
res.status(400).send('Malformed post')
} else if (req.params.event_id != req.body.event_id) {
// Load actual event. Do I want this to be an error? probably
res.status(400).send('Event ID parameter does not match the POST body');
return
} else {
event_id = req.body.event_id
memberIds = req.body.memberIds
}
const deletion_promises = []
event_lineup_entries.filter(entry =>memberIds.includes(entry.memberId.toString())).forEach( entry => {
const promise = teamsnap.deleteEventLineupEntry(entry, teamsnapCallback)
deletion_promises.push(promise)
})
await Promise.all(deletion_promises)
.then(res.status(202).send('OK'))
}

View File

@@ -0,0 +1,100 @@
const tsUtils = require('../lib/utils')
const path = require('path')
exports.partials = path.join(__dirname, "../views/eventsheet/partials")
exports.getEventSheet = async (req,res) =>{
req.promises.push(
teamsnap.loadOpponents(
req.team.id,
(err, opponents)=>{
if (err) console.log("error in route/opponent.js", err);
}
).then(opponents => {req.opponent=opponents.find(o=>o.id==req.event.opponentId);})
)
await Promise.all(req.promises)
req.promises.push(
teamsnap.loadTeamMedia(req.team.id, (err, team_media)=>{
if (err) console.log("error in route/opponent.js", err);
})
.then(team_media => {
req.opponent_logo = team_media.find(tm=>tm.description==`opponent-logo-${req.event.opponentId}.png`)
}
)
)
await Promise.all(req.promises)
const {sheet_size, sheet_layout} = req.query
const {user, team, team_preferences, members, event, event_lineup, event_lineup_entries, availabilities, availabilitySummary, timeline, recent_events, opponent_logo, upcoming_events} = req
res.render('eventsheet/sheet', {sheet_size, sheet_layout, user, team, team_preferences, members, event, event_lineup, event_lineup_entries, availabilities, availabilitySummary, timeline, recent_events, opponent_logo,upcoming_events})
}
exports.getEventSheetBlank = (req,res) => {
res.render('eventsheet/sheet_blank')
}
exports.getLineupCard = (req, res, next) => {
team_id = req.params.team_id;
event_id = req.params.event_id;
teamsnap
.bulkLoad(team_id, [
"team",
"member",
// "member_photos",
"event",
"opponent",
"availabilitySummary",
])
.then((items) => {
events = items.filter((i) => i.type == "event").sort((a, b) => a.startDate - b.startDate);
event = events.find((i) => i.id == event_id);
events_past = events.slice(
events.findIndex((e) => e == event) - 4,
events.findIndex((e) => e == event)
);
events_future = events.slice(events.findIndex((e) => e == event) + 1, events.findIndex((e) => e == event) + 5);
events = events_past.concat(event).concat(events_future);
})
.then((items) => {
return teamsnap.loadAvailabilities({
eventId: events.map((e) => e.id),
}).catch(error => console.log("error in event.js"));
})
.then(() => {
return teamsnap.collections["eventLineups"]
.queryItems("search", {
eventId: events.map((e) => e.id),
})
.then((event_lineups) => {
return Promise.all(event_lineups.map((elu) => elu.loadItem("eventLineupEntries")));
});
})
.then(() => {
items = teamsnap.getAllItems();
events = items.filter((i) => i.type == "event");
current_event_index = events.findIndex((e) => e.id == event_id);
context = {
title: "Gamecard",
team_id: req.params.team_id,
event_id: req.params.event_id,
current_event_index: current_event_index,
events: items.filter((a) => a.type == "event"),
availabilitySummaries: items.filter((i) => i.type == "availabilitySummary"),
event: items.find((e) => e.type == "event" && e.id == event_id),
events_past: events_past,
events_future: events_future,
members: items.filter((a) => a.type == "member"),
availabilities: items.filter((i) => i.type == "availability").sort(tsUtils.teamsnapMembersSortLineupAvailabilityLastName),
all_lineup_entries: items.filter((i) => i.type == "eventLineupEntry"),
event_lineup_entries_offense: items
.filter((i) => i.type == "eventLineupEntry" && i.eventId == event_id && !i.label.has("[PO]"))
.sort((a, b) => a.sequence - b.sequence),
event_lineup_entries: items
.filter((i) => i.type == "eventLineupEntry" && i.eventId == event_id)
.sort((a, b) => a.sequence - b.sequence),
};
res.render("event-lineupcard", context);
});
};

View File

@@ -0,0 +1,9 @@
exports.getMembers = (req, res, next) => {
const {members, team} = req
context = {
title: `Roster`,
team_id: team.id,
team, members,
};
res.render("members", context);
};

109
src/controllers/opponent.js Normal file
View File

@@ -0,0 +1,109 @@
exports.getOpponents = async (req, res, next) => {
const {user, team, layout, csrfToken} = req
opponents = await teamsnap.loadOpponents(team.id, ["event", "availabilitySummary"], (err, opponents)=>{
if (err) console.log('err in controllers/opponent.js')
else return opponents;
})
context = {
title: "Opponents",
user, team, layout, csrfToken,
opponents: opponents,
};
res.render("opponent/list", context);
};
exports.uploadOpponentLogoForm = (req, res, next) => {
opponent_id = req.params.opponent_id;
team_id = req.params.team_id;
res.set("Content-Type", "text/html");
res.render("upload-logo", {
title: "Upload Logo",
csrf_token: req.csrfToken(),
team_id: team_id,
opponent_id: opponent_id,
});
};
exports.uploadOpponentLogo = (req, res, next) => {
opponent_id = req.body.opponent_id;
team_id = req.body.team_id;
member_id = req.user.id;
file = new File(req.file.buffer, `team-logo-${opponent_id}.png`, {
type: "image/png",
});
authTeamsnap(req.user);
teamsnap
.loadCollections()
.then(() => {
return teamsnap.createTeamMedium({
file: file,
mediaFormat: "file",
memberId: member_id,
teamId: team_id,
teamMediaGroupId: "4927028",
description: `team-logo-${opponent_id}.png`,
});
})
.then((item) => {
return teamsnap.uploadTeamMedium(item);
})
.then((item) => {
res.send("Data Received: " + JSON.stringify(item));
})
.fail((err) => console.log(err));
};
exports.getOpponent = async (req, res) => {
await Promise.all(req.promises)
const {team, team_media_group, opponent, layout, opponent_logo, user, csrfToken} = req
context = {
team, team_media_group, opponent, layout, opponent_logo, user, csrfToken
// opponent_logo: items.find(
// (i) => i.type == "teamMedium" && i.description == `opponent-logo-${opponent_id}.png`
// ),
};
res.set("Content-Type", "text/html");
res.render("opponent/show", context);
};
exports.postOpponentLogo = async (req, res, next) => {
res.status('501').send('Not Implemented')
// await Promise.all(req.promises)
// const {team, opponent, user, body} = req
// const filename = `team-logo-${opponent.id}.png`
// file = new File(req.file.buffer, filename, {
// type: "image/png",
// });
// const team_medium = await teamsnap.createTeamMedium(
// {
// file: file,
// memberId: user.id,
// teamId: team.id,
// teamMediaGroupId: body.teamMediaGroupId,
// description: filename,
// }
// )
// await teamsnap.saveTeamMedium(team_medium)
// // await teamsnap.uploadTeamMedium(team_medium)
// const headers={'Authorization': `Bearer ${user.accessToken}`}
// // const url = teamsnap.collections.teamMedia.commands.uploadTeamMedium.href
// const url = teamsnap.collections.teamMedia.queries.search.href
// const response = await fetch(url+`?team_id=${team.id}`, {
// headers,
// method: 'get',
// // body:{team_id:team.id}
// // body: {
// // file: file,
// // member_id: user.id,
// // team_id: team.id,
// // team_media_group_id: body.teamMediaGroupId,
// // description: filename,
// // }
// })
// // await teamsnap
}

42
src/controllers/team.js Normal file
View File

@@ -0,0 +1,42 @@
const { teamsnapCallback } = require("../lib/utils");
utils = require("../lib/utils");
exports.getTeams = async (req, res, next) => {
const {layout, user} = req
const {user_id} = req.params
req.session.current_team_id = null
promise = teamsnap.loadTeams({'userId':user_id},
(err, items) =>{
teamsnapCallback(err,items);
req.teams = items;
})
.fail(
next
)
req.promises.push(promise)
await Promise.all(req.promises)
try {
const context = { layout, title: "Teams", user, teams: req.teams.filter(t=>!t.isRetired) };
res.render("team/list", context);
} catch (e){
next(e);
}
};
exports.getTeamHome = async (req, res, next) => {
await Promise.all(req.promises)
const {user, team, team_preferences, upcoming_events, recent_events, layout} = req
try {
context = {
title: "Home",
layout, team, user, team_preferences, upcoming_events, recent_events
};
res.render("team/home", context);
} catch (e) {
next (e);
}
};

119
src/helpers/eventlineup.js Normal file
View File

@@ -0,0 +1,119 @@
const {embeddedSvgFromPath, parsePositionLabel, compilePositionLabel} = require("../lib/utils")
var hb = require('hbs').create();
const statusCodeIcons = {
1: embeddedSvgFromPath("/teamsnap-ui/assets/icons/check.svg"),
0: embeddedSvgFromPath("/teamsnap-ui/assets/icons/dismiss.svg"),
2: embeddedSvgFromPath("/bootstrap-icons/question-lg.svg"),
null: embeddedSvgFromPath("/bootstrap-icons/question.svg"),
undefined: embeddedSvgFromPath("/bootstrap-icons/question-lg.svg")
}
const statusCodeClasses = {
1: "u-colorPositive",
0: "u-colorNegative",
2: "u-colorPrimary",
null: "u-colorGrey",
undefined: "u-colorGrey"
}
const statusCodeButtonClasses = {
1: "Button--yes",
0: "Button--no",
2: "Button--maybe",
null: "",
undefined: ""
}
exports.flagsString = (flags) => {
return flags != null ? Array.from(flags).join(",") : ''
};
exports.plus1 = (i) => Number(i)+1;
exports.positions = () => ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH", "DR"];
exports.defense_positions = () => ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "P"];
exports.avail_status_code_class = (status_code) => statusCodeButtonClasses[status_code];
exports.avail_status_code_icon = (status_code) => statusCodeIcons[status_code];
exports.positionLabelWithoutFlags = (label) => {
const {positionLabelWithoutFlags} = parsePositionLabel(label);
return positionLabelWithoutFlags
};
exports.positionLabelWithoutPOFlag = (label) => {
const {positionLabelWithoutFlags, positionFlags} = parsePositionLabel(label);
positionFlags.delete('PO')
return compilePositionLabel(positionLabelWithoutFlags, positionFlags)
};
exports.positionFlags = (label)=> {
const {positionFlags} = parsePositionLabel(label);
return `[${Array.from(positionFlags).join(",")}]`
};
exports.hasPositionFlags = (label) => {
const {positionLabelWithoutFlags, positionFlags} = parsePositionLabel(label);
return positionFlags.size > 0;
};
exports.comparePositionWithFlags = (labelWithoutFlags, eventLineupEntry, options) => {
labelWithFlags = eventLineupEntry?.label
const {positionLabelWithoutFlags} = parsePositionLabel(labelWithFlags);
return positionLabelWithoutFlags == labelWithoutFlags;
};
exports.isStarting = (member) => {
return (member.benchcoach?.eventLineupEntry != null);
};
exports.isInStartingLineup = (member) => {
if (member.benchcoach.eventLineupEntry == null || member.benchcoach.eventLineupEntry.label == '') return false;
const {positionFlags} = parsePositionLabel(member.benchcoach.eventLineupEntry?.label);
return (!positionFlags.has("PO"))
};
exports.isInPositionOnly = (member) => {
if (!member.benchcoach || member.benchcoach.eventLineupEntry == null) return false;
const {positionFlags} = parsePositionLabel(member.benchcoach.eventLineupEntry?.label);
return (member.benchcoach.eventLineupEntry != null && positionFlags.has("PO"))
};
exports.isInBench = (member) => {
if ((member.benchcoach.eventLineupEntry != null && member.benchcoach.eventLineupEntry.label != '') || member.isNonPlayer) return false;
return (member.benchcoach.availability?.statusCode != 0 && member.benchcoach.availability?.statusCode != null)
};
exports. isInOut = (member) => {
if ((member.benchcoach.eventLineupEntry != null && member.benchcoach.eventLineupEntry.label != '') || member.isNonPlayer) return false;
return (member.benchcoach.availability?.statusCode == 0 || member.benchcoach.availability?.statusCode == null)
};
exports.availabilityStatusShort = (availability) => {
const {YES, MAYBE, NO, NONE} = teamsnap.AVAILABILITIES
const statusShortLookup = {}
statusShortLookup[YES] = "YES"
statusShortLookup[MAYBE] = "MAY"
statusShortLookup[NO] = "NO"
statusShortLookup[NONE] = "UNK"
statusShortLookup[undefined] = "UNK"
return (statusShortLookup[availability?.statusCode])
};
exports.filterNonPlayers = (members) => {
return members.filter(m=>!m.isNonPlayer)
};
exports.joinMemberEmailAddresses = (members) => {
return members.map(m=>m.emailAddresses.join(',')).join(',')
}
exports.loadSlots = (options) =>{
var s = ""
const {members, event_lineup, event_lineup_entries, event, availabilities} = options.data.root
event_lineup_entries.forEach(eventLineupEntry =>{
const availability = availabilities?.find(a=>a.memberId==eventLineupEntry.memberId)
const member = members.find(m=>m.id==eventLineupEntry.memberId)
const {positionFlags} = parsePositionLabel(eventLineupEntry.label)
const initial_lineup_segment = `${positionFlags.has('PO') ? 'position-only' : 'starting'}`
s+=options.fn({eventLineupEntry, availability, member, event, initial_lineup_segment})
})
const players_without_lineup_entry = members.filter(
member=>!event_lineup_entries.map(lue=>lue.memberId).includes(member.id) && !member.isNonPlayer
)
players_without_lineup_entry.forEach(member =>{
const availability = availabilities?.find(a=>a.memberId==member.id)
let initial_lineup_segment
if (availability?.statusCode == 0 || availability?.statusCode == null) {
initial_lineup_segment =`out`
} else {
initial_lineup_segment =`bench`
}
s+=options.fn({availability, member, event, initial_lineup_segment})
})
return s
}

156
src/helpers/eventsheet.js Normal file
View File

@@ -0,0 +1,156 @@
const { parsePositionLabel, teamsnapMembersSortLineupAvailabilityLastName, teamsnapMembersSortAvailabilityLastName } = require('../lib/utils')
const {attachBenchcoachPropertiesToMember} = require('../controllers/eventlineup')
const Handlebars = require("handlebars");
exports.offenseLineup = (number_of_slots, event_lineup_entries, members, options) => {
var results = ""
// const {event_lineup_entries, members} = options.data.root
for (let i = 0; i < number_of_slots; i++){
const event_lineup_entry = event_lineup_entries ? event_lineup_entries[i] : null
if (event_lineup_entry && !parsePositionLabel(event_lineup_entry.label).positionFlags.has('PO')){
results += options.fn({
sequence: event_lineup_entry.sequence,
member: members.find(member=> event_lineup_entry.memberId == member.id || event_lineup_entry.memberName == `${member.firstName} ${member.lastName}`),
label: event_lineup_entry.label
})
}
else {
results += options.fn({
sequence: i,
member: {},
label: ""
})
}
}
return results
}
exports.defenseLineup = (event_lineup_entries, members, options) => {
var results = ""
// const {event_lineup_entries, members} = options.data.root
const positions = ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "P"]
positions.forEach(position=>{
const event_lineup_entry = event_lineup_entries ? event_lineup_entries.find(e=>parsePositionLabel(e.label).positionLabelWithoutFlags == position) : null
if (event_lineup_entry) {
results += options.fn({position, eventLineupEntry:event_lineup_entry, member:members.find(member=> event_lineup_entry.memberId == member.id || event_lineup_entry.memberName == `${member.firstName} ${member.lastName}`)})
}
else {
results += options.fn({position, member:{}})
}
})
return results
}
exports.rosterHistoryHeader = (options) => {
var results = ""
events = ["+1", "+2", "+3", "+4","-1","-2","-3","-4"]
events.forEach(event => {
const class_name = event.includes("+") ? "plus": "minus"
const past_or_future = event.includes("+") ? "future": "past"
const index = Number(event.replace("+","").replace("-",""))
results += options.fn({class:`today-${class_name}-${index} ${past_or_future}`, event})
})
return results;
}
exports.rosterHistory = (event, event_lineup_entries, members, availabilities, options) => {
var results = ""
// const {event, event_lineup_entries, members, availabilities} = options.data.root
const players = members.filter(m=>!m.isNonPlayer)
attachBenchcoachPropertiesToMember(players, event_lineup_entries ? event_lineup_entries.filter(i=>i.eventId==event.id) : [], availabilities.filter(i=>i.eventId==event.id))
players.sort(teamsnapMembersSortLineupAvailabilityLastName)
players.forEach(member=>{
const {firstName, lastName, jerseyNumber, benchcoach, position, id} = member
results += options.fn({
id, firstName, lastName, jerseyNumber, position, benchcoach
})
}
)
return results;
}
const positionGroups = {
"P":"P",
"IF":"IF",
"1B":"IF",
"2B":"IF",
"3B":"IF",
"SS":"IF",
"OF":"OF",
"LF":"OF",
"CF":"OF",
"RF":"OF",
"C":"C"
}
exports.positionCapabilityFor = (member, position, options) => {
if (!member.position) {
return ""
}
const member_positions = member.position.split(",").map(s=>s.trim())
const member_position_groups = new Set(member.position.split(",").map(s=>positionGroups[s.trim()]))
if (member_position_groups.has(position)){
return "\u2713"
}
else {
return ""
}
}
exports.firstLetter = (s, options) => {
return s[0];
}
exports.repeat = (n, options) => {
var results = "";
[...Array(n).keys()].forEach(i => {
results += options.fn({index: i})
});
return results;
}
exports.loopEvents = (events, options) => {
var results = "";
if (options.data) {
data = Handlebars.createFrame(options.data);
}
events.forEach((event,i) => {
if (data) {
data.index = i;
}
results += options.fn(event, {data: data })
}
)
return results;
}
exports.timepointForMember = (member, timeline, event, options) => {
var results = ""
if (options.data) {
data = Handlebars.createFrame(options.data);
}
const availability = timeline.availabilities.find(a=>a.memberId==member.id && a.eventId==event.id)
const eventLineupEntry = timeline.event_lineup_entries.find(a=>(a.memberId==member.id || a.memberName == `${member.firstName} ${member.lastName}`) && a.eventId==event.id)
var value = ""
if (eventLineupEntry){
value = parsePositionLabel(eventLineupEntry.label).positionLabelWithoutFlags
}
else {
value = availability?.status[0]
}
return options.fn({availability: availability, eventLineupEntry: eventLineupEntry, value}, {data: data })
}
exports.ifEquals = (testValue, targetValue, options) => {
if (testValue === targetValue) {
return options.fn();
} else {
return '';
}
}

180
src/lib/utils.js Normal file
View File

@@ -0,0 +1,180 @@
const path = require('path')
const fs = require('fs')
exports.teamsnapMembersSortLineupAvailabilityLastName = (a, b) => {
status_code_sort = [
teamsnap.AVAILABILITIES.YES,
teamsnap.AVAILABILITIES.MAYBE,
teamsnap.AVAILABILITIES.NO,
teamsnap.AVAILABILITIES.NONE,
];
if (a.benchcoach.eventLineupEntry != null && b.benchcoach.eventLineupEntry != null){
return a.benchcoach.eventLineupEntry.sequence - b.benchcoach.eventLineupEntry.sequence
}
else if (a.benchcoach.eventLineupEntry != null && b.benchcoach.eventLineupEntry == null){
return -1
}
else if (a.benchcoach.eventLineupEntry == null && b.benchcoach.eventLineupEntry != null) {
return 1
}
else {
return teamsnapMembersSortAvailabilityLastName(a,b)
}
};
teamsnapMembersSortAvailabilityLastName = (a, b) => {
status_code_sort = [
teamsnap.AVAILABILITIES.YES,
teamsnap.AVAILABILITIES.MAYBE,
teamsnap.AVAILABILITIES.NO,
teamsnap.AVAILABILITIES.NONE,
];
a_sort = status_code_sort.indexOf(a.benchcoach.availability?.statusCode);
b_sort = status_code_sort.indexOf(b.benchcoach.availability?.statusCode);
if (a_sort > b_sort) {
return 1;
}
if (a_sort < b_sort) {
return -1;
}
if (a_sort == b_sort) {
if (a.lastName < b.lastName) {
return -1;
}
if (a.lastName > b.lastName) {
return 1;
}
}
}
exports.teamsnapMembersSortAvailabilityLastName = teamsnapMembersSortAvailabilityLastName
exports.teamsnapCallback = (err,result, d) => {
if (Array.isArray(result)){
types = new Set(result.map(i=>i.type))
}
else {
types = [result?.type]
}
if (d) {
console.log(
'\x1b[33mTeamSnap:\x1b[0m',
`${d.source} using ${d.method ? "teamsnap."+d.method : "?"} \x1b[33m\[${Array.from(types).join(", ")}\]\x1b[0m`
)
}
if (err) {
console.log(err.message);
throw new Error(err)
}
return result;
}
exports.teamsnapFailure = (err, next) => {
if (err) {
console.log(err.message);
}
next(err);
}
const getPluralType = (type) =>{
// There are some instances where a type is not
// in the list of teamsnap.types, so a plural
// is not generated in the lookup. this is a
// kludge around that. (specifically availabilitySummary)
plural = teamsnap.getPluralType(type) || (function() {
if (type === undefined){
return type
}
switch (type.slice(-1)) {
case 'y':
return type.slice(0, -1) + 'ies';
case 's':
return type + 'es';
default:
return type + 's';
}
})();
return plural
}
exports.groupTeamsnapItems = (items, types = [], params = {}) => {
const result = {};
items.forEach(item => {
const type = item.type
const type_plural = getPluralType(type)
if ((types.length > 0 && types.includes(type)) || (types.length == 0)) {
if (!result[type_plural]) result[type_plural] = []
result[type_plural].push(item)
}
})
return result;
}
exports.embeddedSvgFromPath = (svg_path, additional_classes, options) => {
const iconStaticPaths = {
"/teamsnap-ui/assets":path.join(__dirname, "/../../node_modules/@teamsnap/teamsnap-ui/src/assets"),
"/bootstrap-icons":path.join(__dirname, "/../../node_modules/bootstrap-icons/icons"),
"/media":path.join(__dirname, "/../public/media")
}
for (const [key, value] of Object.entries(iconStaticPaths)) {
if (svg_path.startsWith(key)) {
svg_path = svg_path.replace(key, value)
}
}
if (!options) {options=additional_classes; additional_classes=''}
const svg = fs.readFileSync(`${svg_path}`, 'utf8');
svgRegExWithClass = new RegExp(/<svg(.*)class="(.*?)"(.*)>/)
svgRegExWithoutClass = new RegExp(/<svg(.*?)>/)
if (svgRegExWithClass.test(svg)) {
return svg.replace(svgRegExWithClass, `<svg$1 class="$2 Icon ${additional_classes}"$3>`)
}
else if (svgRegExWithoutClass.test(svg)) {
return svg.replace(svgRegExWithoutClass, `<svg$1 class="Icon ${additional_classes}">`)
}
else return svg
}
exports.parsePositionLabel = (label) => {
if (label == null) {
return {
positionLabelWithoutFlags: null,
positionFlags: null
}
}
const pattern = /(?<pos>[A-Z0-9]+)(?:\s\[(?<flags>.[A-z,]+)\])?/g
const {pos, flags} = pattern.exec(label)?.groups || {}
const positionLabelWithoutFlags= pos
const positionFlags = new Set(flags?.split(',').map(f=>f.trim()) || [])
return {positionLabelWithoutFlags, positionFlags}
}
exports.compilePositionLabel = (label, flags) => {
if (flags == null || flags == '' || flags.size == 0) {
return label
}
else {
const flags_set = toFlagsSet(flags)
return `${label} [${Array.from(flags_set).sort().join(',')}]`
}
}
function toFlagsSet(flags) {
let flags_set
if (typeof(flags) == 'string'){
flags_set = new Set(flags.split(',').map(s=>s.trim()))
} else if (flags.constructor === Array){
flags_set = new Set(flags)
} else if (flags.constructor === Set){
flags_set = flags
}
return flags_set
}

View File

@@ -0,0 +1,51 @@
exports.loadRecentAndUpcomingEvents = async (req, res, next) => {
const {team_id, event_id} = req.params
const page_size = req.query.page_size ? Number(req.query.page_size) : 4
var subject_date
if (event_id) {
const event = await teamsnap.loadEvents({id: event_id}).pop()
const new_date = new Date(event.startDate.getTime()+10000);
subject_date = event.startDate
}
else {
subject_date = new Date()
}
req.promises.push(
teamsnap.bulkLoad({
teamId: team_id,
types: ["event", "availabilitySummary"],
scopeTo: "event",
event__startedAfter: new Date(subject_date.getTime()+10000),
event__pageSize: page_size
})
.then(items => tsUtils.groupTeamsnapItems(items))
.then((items)=>{
req.upcoming_events=items.events ? items.events : [];
const availabilitySummaries=items.availabilitySummaries;
req.upcoming_events.forEach((event) => {
event.link('availabilitySummary', availabilitySummaries.find(a=>a.eventId==event.id))
})
}
).fail(utils.teamsnapFailure)
)
req.promises.push(
teamsnap.bulkLoad({
teamId: team_id,
types: ["event", "availabilitySummary"],
scopeTo: "event",
event__startedBefore: new Date(subject_date.getTime()-10000),
event__pageSize: page_size,
event__sortStartDate: "desc"
})
.then(items => tsUtils.groupTeamsnapItems(items))
.then((items)=>{
req.recent_events=items.events || [];
const availabilitySummaries=items.availabilitySummaries;
req.recent_events.forEach((event) => {
event.link('availabilitySummary', availabilitySummaries.find(a=>a.eventId==event.id))
})
}
).fail(utils.teamsnapFailure)
)
next();
}

18
src/middlewares/csrf.js Normal file
View File

@@ -0,0 +1,18 @@
const { doubleCsrf } = require('csrf-csrf');
const csrf = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
getTokenFromRequest: req => {
return req.body.csrfToken
},
cookieName: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'development' ? '__benchcoach.x-csrf-token' : '_csrf',
cookieOptions: {
secure: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'development' // Enable for HTTPS in production
}
});
module.exports = {
doubleCsrfProtection: csrf.doubleCsrfProtection,
generateToken: csrf.generateToken
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,908 @@
@charset "UTF-8";
@import url("https://fonts.googleapis.com/css2?family=Pacifico");
@import url("https://fonts.googleapis.com/css2?family=Oswald");
@import url("https://fonts.googleapis.com/css2?family=Graduate");
@import url("https://fonts.googleapis.com/css2?family=Inconsolata:wdth,wght@50..200,200..900&display=swap");
@import url("/font/helvetica-now/stylesheet.css");
@import url("/font/futura-now/stylesheet.css");
:root {
--color-success: #b7e1cd;
--color-danger: #f4c7c3;
--color-neutral: #acc9fe;
--color-warning: rgb(249, 228, 180);
--color-grey-100: #f8f9fa;
--color-grey-200: #e9ecef;
--color-grey-300: #dee2e6;
--color-grey-400: #ced4da;
--color-grey-500: #adb5bd;
--color-grey-600: #6c757d;
--color-grey-700: #495057;
--color-grey-800: #343a40;
--color-grey-900: #212529;
--header-height: 17px;
--row-height: 14px;
--monospace-font: "Inconsolata", monospace;
--section-border: 0.5px solid black;
}
/** For Print **/
@media print {
:root {
margin: 0;
}
body {
margin: 0;
}
body .sheet {
padding: 0.175in;
background: white;
}
}
/** For screen preview **/
@media screen {
body .sheet {
padding: 0.175in;
}
body {
background: #e0e0e0;
}
.sheet {
margin: auto;
margin-bottom: 12px;
box-shadow: 0 0.5mm 2mm rgba(0, 0, 0, 0.3);
}
}
.sheet {
overflow: hidden;
position: relative;
box-sizing: border-box;
page-break-after: always;
background: white;
}
/** Paper sizes **/
body.B5 .sheet {
width: 176mm;
height: 250mm;
}
body.index-card .sheet {
width: 3.5in;
height: 5in;
}
body.letter .sheet {
width: 8.5in;
height: 11in;
}
body {
font-family: "Helvetica Now", "Helvetica", sans-serif;
position: relative;
font-size: 11px;
text-transform: uppercase;
}
table, #roster-and-history table, .lineup-card table {
font-size: inherit;
border-collapse: collapse;
empty-cells: show;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
}
table th, #roster-and-history table th, .lineup-card table th {
color: var(--color-grey-700);
}
table th, #roster-and-history table th, .lineup-card table th, table td, table #roster-and-history .position, #roster-and-history table .position, #roster-and-history table td, .lineup-card table td {
overflow: hidden;
padding: 0 2px 0 2px;
}
table th:empty::after, #roster-and-history table th:empty::after, table td:empty::after, table #roster-and-history .position:empty::after, #roster-and-history table .position:empty::after, #roster-and-history table td:empty::after {
content: " ";
}
table.striped tr:nth-child(odd) td, table.striped tr:nth-child(odd) #roster-and-history .position, #roster-and-history table tr:nth-child(odd) td, #roster-and-history table tr:nth-child(odd) .position, .lineup-card table tr:nth-child(odd) td, .lineup-card table tr:nth-child(odd) #roster-and-history .position, table.striped tr:nth-child(odd) th, #roster-and-history table tr:nth-child(odd) th, .lineup-card table tr:nth-child(odd) th {
background-color: whitesmoke;
}
table.striped tr:nth-child(even) td, table.striped tr:nth-child(even) #roster-and-history .position, #roster-and-history table tr:nth-child(even) td, #roster-and-history table tr:nth-child(even) .position, .lineup-card table tr:nth-child(even) td, .lineup-card table tr:nth-child(even) #roster-and-history .position, table.striped tr:nth-child(even) th, #roster-and-history table tr:nth-child(even) th, .lineup-card table tr:nth-child(even) th {
background-color: white;
}
.float-right {
float: right;
}
.float-left {
float: left;
}
.eventsheet {
--page-margin: 0.175in;
}
.eventsheet:has(section) {
display: grid;
}
.eventsheet:has(section) section {
--divider-border: lightgrey dashed 1px;
box-sizing: content-box;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: stretch;
outline-style: solid;
outline-width: calc(var(--divider-border) / 4);
outline-color: lightgray;
}
.eventsheet:has(section) section > div {
display: flex;
flex: 1;
align-items: stretch;
height: auto;
width: 100%;
}
.eventsheet:has(section) section.NW {
grid-area: 1/1/2/2;
}
.eventsheet:has(section) section.NE {
grid-area: 1/2/2/3;
}
.eventsheet:has(section) section.SW {
grid-area: 2/1/3/2;
}
.eventsheet:has(section) section.SE {
grid-area: 2/2/3/3;
}
.eventsheet:has(section) section.NW .divider, .eventsheet:has(section) section.SW .divider {
border-right: var(--divider-border);
}
.eventsheet:has(section) section.NW .divider, .eventsheet:has(section) section.NE .divider {
border-bottom: var(--divider-border);
}
.eventsheet.quarters {
--section-margin: calc(var(--page-margin)/2);
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
column-gap: calc(var(--page-margin) * 2);
row-gap: calc(var(--page-margin) * 2);
outline-offset: var(--section-margin);
}
.eventsheet.quarters section {
outline-offset: var(--page-margin);
}
.letter .eventsheet.quarters {
--header-height: 0.5in;
}
.letter .eventsheet.index-cards-4x6 {
--section-margin: calc(var(--page-margin)/2);
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 125mm;
column-gap: calc(var(--page-margin) * 2);
row-gap: calc(var(--page-margin) * 2);
}
.letter .eventsheet.index-cards-4x6 section {
outline-offset: var(--page-margin);
}
.letter .eventsheet.index-cards-3x5 {
--section-margin: calc(var(--page-margin)/2);
grid-template-columns: 3in 3in;
grid-template-rows: 5in 5in;
column-gap: calc(var(--page-margin) * 2);
row-gap: calc(var(--page-margin) * 2);
}
.letter .eventsheet.index-cards-3x5 section {
outline-offset: var(--page-margin);
}
.lineup-card {
counter-reset: lineup-sequence-counter 0;
--border: 0.5px solid grey;
border: var(--section-border);
}
.lineup-card header {
text-transform: uppercase;
border-style: none;
border-bottom: var(--border);
height: var(--header-height);
}
.lineup-card header:empty::after {
content: " ";
}
.lineup-card th {
width: inherit;
}
.lineup-card th.sequence {
counter-increment: lineup-sequence-counter 1;
color: var(--color-grey-600);
font-size: inherit;
width: 2ch;
font-stretch: 50%;
border-right: var(--border);
}
.lineup-card th.sequence.counter::before {
content: counter(lineup-sequence-counter);
}
.lineup-card thead th {
color: var(--color-grey-600);
font-size: 0.7em;
border-bottom: var(--border);
}
.lineup-card table, .lineup-card #roster-and-history table, #roster-and-history .lineup-card table {
font-size: 21px;
}
.lineup-card td, .lineup-card #roster-and-history .position, #roster-and-history .lineup-card .position {
/* height: 34px; */
}
.lineup-card td.substitution, .lineup-card #roster-and-history .substitution.position, #roster-and-history .lineup-card .substitution.position {
width: 8ch;
}
.lineup-card td.substitution::after, .lineup-card #roster-and-history .substitution.position::after, #roster-and-history .lineup-card .substitution.position::after {
content: "";
}
.lineup-card td.position, .lineup-card #roster-and-history .position, #roster-and-history .lineup-card .position, .lineup-card td.jersey-number {
width: 2ch;
}
.lineup-card td.position, .lineup-card #roster-and-history .position, #roster-and-history .lineup-card .position, .lineup-card td.jersey-number, .lineup-card td.substitution {
font-family: var(--monospace-font);
border-left: var(--border);
text-align: right;
padding-left: 2.5px;
padding-right: 2.5px;
}
.lineup-card tr + tr td, .lineup-card tr + tr #roster-and-history .position, #roster-and-history .lineup-card tr + tr .position, .lineup-card tr + tr th {
border-top: var(--border);
}
.lineup-card.dugout td.player-name, .lineup-card.dugout #roster-and-history .player-name.position, #roster-and-history .lineup-card.dugout .player-name.position {
width: 10ch;
font-stretch: 75%;
}
.lineup-card.dugout .position, .lineup-card.dugout .jersey-number, .lineup-card.dugout .substitution {
font-stretch: 75%;
}
.lineup-card.exchange header {
text-align: center;
}
.lineup-card.exchange header .float-left, .lineup-card.exchange header .float-right {
float: none;
}
.lineup-card.exchange .player-name {
font-stretch: 100%;
}
.lineup-card.exchange .homeaway, .lineup-card.exchange .substitution {
display: none;
}
section.blank svg, section.blank header {
filter: grayscale(1) opacity(0.4);
}
section.blank > div {
filter: opacity(0.4);
}
section.blank > div td.substitution, section.blank > div #roster-and-history .substitution.position, #roster-and-history section.blank > div .substitution.position {
border-width: 0.5;
}
#todays-game > div {
display: grid;
grid-template-columns: 110px auto;
grid-template-rows: auto auto;
grid-template-areas: "offense defense" "footer footer";
}
#todays-game #offense-pane {
grid-area: offense;
}
#todays-game #defense-pane {
grid-area: defense;
}
#todays-game .footer {
/* height:var(--row-height); */
position: relative;
box-sizing: border-box;
grid-area: footer;
/* border: 1px solid black; */
height: 100%;
border-right: 0.5px solid grey;
border-left: 0.5px solid grey;
}
#todays-game .footer table {
height: 100%;
outline: none;
border-style: none;
}
#todays-game .footer table tr td, #todays-game .footer table tr #roster-and-history .position, #roster-and-history #todays-game .footer table tr .position, #todays-game .footer table tr th {
background-color: white;
outline: none;
border-bottom: 0.5px solid var(--color-grey-500);
}
#todays-game .footer table tr :last-child td, #todays-game .footer table tr :last-child #roster-and-history .position, #roster-and-history #todays-game .footer table tr :last-child .position {
background-color: white;
outline: none;
border-bottom-style: none;
}
#todays-game .footer table th {
text-align: left;
color: var(--color-grey-600);
}
#todays-game .footer table td, #todays-game .footer table #roster-and-history .position, #roster-and-history #todays-game .footer table .position {
height: var(--row-height);
border: none;
}
#todays-game .footer table tdempty::after {
content: "";
}
#todays-game table.notes th {
border-left: none;
border-right: none;
line-height: 1em;
}
#todays-game table.notes td:empty::after, #todays-game table.notes #roster-and-history .position:empty::after, #roster-and-history #todays-game table.notes .position:empty::after {
content: "";
}
#defense-card #defense-pane {
width: 100%;
}
#defense-card .footer {
display: none;
}
#defense-card .slot-set table {
font-size: 14px;
width: 120px;
}
#defense-card .slot-set.pos-c {
grid-area: 6/1/7/5 !important;
}
#defense-card .slot-set.pos-p {
justify-content: center !important;
align-items: center;
margin-bottom: inherit !important;
grid-area: 5/1/6/5 !important;
}
#defense-card .slot-set.pos-p table {
width: 120px !important;
}
#defense-card .slot-set.pos-p table tbody > tr:last-child {
display: table-row;
}
#defense-card {
border: var(--section-border);
}
#defense-pane {
position: relative;
grid-area: defense;
padding: 4px 4px 0px 4px; /* top right bottom left */
display: flex;
}
#defense-pane .field-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(6, 1fr);
grid-column-gap: 4px;
width: 100%;
z-index: 2;
}
#defense-pane svg {
position: absolute;
stroke-linecap: round;
stroke-miterlimit: 1.5;
z-index: -1;
opacity: 70%;
}
#defense-pane svg #outfield-path {
stroke: #4AA1D5;
fill: none;
stroke-width: 4px;
}
#defense-pane svg #infield-path {
stroke: #4AA1D5;
fill: #D1E6F7;
stroke-width: 4px;
fill-opacity: 50%;
}
#defense-pane .slot-set {
display: flex;
align-items: center;
}
#defense-pane .slot-set .player-name {
font-stretch: 80%;
}
#defense-pane .slot-set table {
--border: grey solid 0.5px;
border: var(--border);
opacity: 85%;
}
#defense-pane .slot-set table tr:first-child th {
border-bottom: var(--border);
}
#defense-pane .slot-set table tr + tr td, #defense-pane .slot-set table tr + tr #roster-and-history .position, #roster-and-history #defense-pane .slot-set table tr + tr .position, #defense-pane .slot-set table tr + tr th {
border-top: var(--border);
}
#defense-pane .slot-set table tr th.position {
font-family: var(--monospace-font);
width: 2ch;
text-align: right;
}
#defense-pane .slot-set.pos-cf tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-cf tr:first-child th.position:empty::after {
content: "cf";
}
#defense-pane .slot-set.pos-lf tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-lf tr:first-child th.position:empty::after {
content: "lf";
}
#defense-pane .slot-set.pos-rf tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-rf tr:first-child th.position:empty::after {
content: "rf";
}
#defense-pane .slot-set.pos-ss tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-ss tr:first-child th.position:empty::after {
content: "ss";
}
#defense-pane .slot-set.pos-2b tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-2b tr:first-child th.position:empty::after {
content: "2b";
}
#defense-pane .slot-set.pos-3b tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-3b tr:first-child th.position:empty::after {
content: "3b";
}
#defense-pane .slot-set.pos-1b tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-1b tr:first-child th.position:empty::after {
content: "1b";
}
#defense-pane .slot-set.pos-c tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-c tr:first-child th.position:empty::after {
content: "c";
}
#defense-pane .slot-set.pos-p tr:first-child th.position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-p tr:first-child th.position:empty::after {
content: "p";
}
#defense-pane .slot-set.pos-cf {
justify-content: center;
grid-area: 1/1/2/5;
}
#defense-pane .slot-set.pos-lf {
justify-content: flex-start;
grid-area: 2/1/3/3;
}
#defense-pane .slot-set.pos-rf {
justify-content: flex-end;
grid-area: 2/3/3/5;
}
#defense-pane .slot-set.pos-ss {
justify-content: flex-end;
grid-area: 3/1/4/3;
}
#defense-pane .slot-set.pos-2b {
justify-content: flex-start;
grid-area: 3/3/4/5;
}
#defense-pane .slot-set.pos-3b {
justify-content: flex-start;
grid-area: 4/1/5/3;
}
#defense-pane .slot-set.pos-1b {
justify-content: flex-end;
grid-area: 4/3/5/5;
}
#defense-pane .slot-set.pos-c {
justify-content: center;
grid-area: 5/1/6/5;
}
#defense-pane .slot-set.pos-p {
align-items: end;
margin-bottom: 4px;
grid-area: 6/1/7/5;
}
#defense-pane .slot-set.pos-p table {
width: 100%;
}
#defense-pane .slot-set.pos-p tr.substitute .position:empty {
border-right: var(--border);
}
#defense-pane .slot-set.pos-p tr.substitute .position:empty::after {
content: "RP";
}
#offense-pane {
position: relative;
/* box-sizing: border-box; */
height: 100%;
border-bottom: 0.5px solid black;
counter-reset: lineup-sequence-counter 0;
/* outline: 0.5px solid black; */
}
#offense-pane table {
height: 100%;
border: none;
}
#offense-pane th.sequence {
counter-increment: lineup-sequence-counter 1;
}
#offense-pane th.sequence.counter::before {
content: counter(lineup-sequence-counter);
}
header {
background-color: #cadcf9;
height: var(--header-height);
width: auto;
text-align: center;
padding-left: 10px;
padding-right: 10px;
border-bottom: var(--section-border);
}
.cell-checkbox {
font-size: 0.75em;
}
.in-starting-lineup {
font-weight: bold;
}
.event-title {
font-stretch: semi-condensed;
}
.homeaway {
font-weight: 900;
}
.cell-smalltext {
font-stretch: condensed;
font-size: 10px;
}
.statscell {
font-family: "m+1m";
text-align: center;
font-stretch: extra-condensed;
font-size: 9px;
width: 60px;
}
.condensedNameCell {
width: 70px;
text-transform: uppercase;
font-stretch: condensed;
}
.cell-square {
height: var(--row-height);
width: 14px;
text-align: center;
}
.cell-square.narrow {
width: 10px;
}
.cell-mono {
font-family: "m+1m";
}
.cell-condensed {
font-stretch: condensed;
}
#roster-and-history {
--border: var(--section-border);
}
#roster-and-history table tr td.available-status-code-1, #roster-and-history table tr .available-status-code-1.position {
color: rgb(0, 85, 0);
background-color: #b7e1cd;
}
#roster-and-history table tr td.available-status-code-0, #roster-and-history table tr .available-status-code-0.position {
color: rgb(170, 0, 0);
background-color: #f4c7c3;
}
#roster-and-history table tr td.past.available-status-code-0, #roster-and-history table tr .past.available-status-code-0.position, #roster-and-history table tr td.past.available-status-code-null, #roster-and-history table tr .past.available-status-code-null.position {
color: var(--color-grey-600);
background-color: inherit;
}
#roster-and-history table tr td.past.available-status-code-1.Y, #roster-and-history table tr .past.available-status-code-1.Y.position {
color: inherit;
background-color: var(--color-warning);
}
#roster-and-history table tr td.available-status-code-2, #roster-and-history table tr .available-status-code-2.position {
color: blue;
background-color: #acc9fe;
}
#roster-and-history table thead tr {
border: black solid 1px;
height: var(--header-height);
}
#roster-and-history > div > table {
/* font-size: 10.5px; */
padding: 0;
line-height: 1em;
/* outline: 0.5px black; */
}
#roster-and-history tr.starting-today td.jersey-number, #roster-and-history tr.starting-today .jersey-number.position, #roster-and-history tr.starting-today td.player-name, #roster-and-history tr.starting-today .player-name.position {
font-weight: bold;
}
#roster-and-history .player-name {
font-stretch: 95%;
}
#roster-and-history .jersey-number {
font-family: var(--monospace-font);
width: 2ch;
text-align: right;
overflow: hidden;
}
#roster-and-history tr + tr {
border-top: var(--border);
}
#roster-and-history td, #roster-and-history .position, #roster-and-history th {
border-left: none;
border-right: none;
padding: 0.2em 0.1em 0.2em 0.1em; /* top right bottom left */
}
#roster-and-history td.availability-on-day, #roster-and-history .position, #roster-and-history th.availability-on-day {
font-family: var(--monospace-font);
font-stretch: 60%;
text-align: center;
max-width: 0.8em;
min-width: 0.8em;
}
#roster-and-history td.availability-on-day.future, #roster-and-history .future.position, #roster-and-history td.availability-on-day.past, #roster-and-history .past.position, #roster-and-history th.availability-on-day.future, #roster-and-history th.availability-on-day.past {
font-family: var(--monospace-font);
font-stretch: condensed;
font-weight: normal;
font-size: 0.8em;
padding: 0.1em;
text-transform: uppercase;
}
#roster-and-history td.position-capability, #roster-and-history .position-capability.position, #roster-and-history th.position-capability {
font-size: 8px;
font-stretch: 50%;
width: 5px;
text-align: center;
padding: 0;
}
#roster-and-history td.spacer, #roster-and-history .spacer.position, #roster-and-history th.spacer {
display: none;
}
#roster-and-history td.spacer.first-of-group, #roster-and-history .spacer.first-of-group.position, #roster-and-history td.spacer.last-of-group, #roster-and-history .spacer.last-of-group.position, #roster-and-history th.spacer.first-of-group, #roster-and-history th.spacer.last-of-group {
border: none;
}
#roster-and-history td.player-stats, #roster-and-history .player-stats.position, #roster-and-history th.player-stats {
display: none;
font-family: var(--monospace-font);
font-size: 1em;
font-stretch: 60%;
font-weight: 300;
}
#roster-and-history td.player-stats .delimiter, #roster-and-history .player-stats.position .delimiter,
#roster-and-history td.player-stats .decimal-point,
#roster-and-history .player-stats.position .decimal-point, #roster-and-history th.player-stats .delimiter,
#roster-and-history th.player-stats .decimal-point {
font-family: Helvetica Now;
font-stretch: expanded;
color: var(--color-grey-500);
}
#roster-and-history td.player-stats .decimal-point, #roster-and-history .player-stats.position .decimal-point, #roster-and-history th.player-stats .decimal-point {
color: rgba(0, 0, 0, 0);
}
#roster-and-history td.player-stats .delimiter, #roster-and-history .player-stats.position .delimiter, #roster-and-history th.player-stats .delimiter {
color: var(--color-grey-500);
}
#roster-and-history td.player-name, #roster-and-history .player-name.position {
color: black !important;
text-align: left;
font-stretch: 95%;
}
#roster-and-history td.jersey-number, #roster-and-history .jersey-number.position {
color: black !important;
}
#roster-and-history .first-of-group {
border-left-width: 1px;
border-left-style: solid;
border-left-color: black;
}
#roster-and-history .last-of-group {
border-right-width: 1px;
border-right-style: solid;
border-right-color: black;
}
#roster-and-history col.player-stats {
border: inherit;
}
#roster-and-history table tr:nth-child(odd) th {
background-color: #cadcf9;
color: black;
}
#roster-and-history table tr:nth-child(odd) th.availability-on-day div, #roster-and-history table tr:nth-child(odd) th.position div {
transform: rotate(270deg);
/* font-stretch: 40%; */
font-stretch: 75%;
font-weight: 500;
text-align: left;
}
#roster-and-history thead > tr, #roster-and-history tfoot > tr {
border-bottom: solid black 1px;
}
#roster-and-history tbody {
border-bottom: solid black 1px;
}
#roster-and-history tr.border-top td, #roster-and-history tr.border-top .position, #roster-and-history tr.border-top th {
border-top: 1px solid black;
}
.letter .eventsheet.quarters header {
font-size: xx-large;
}
.letter .eventsheet.quarters .lineup-card table, .letter .eventsheet.quarters .lineup-card #roster-and-history table, #roster-and-history .letter .eventsheet.quarters .lineup-card table, .letter .eventsheet.quarters #roster-and-history .lineup-card table {
font-size: 23;
}
.letter .eventsheet.quarters #defense-pane .slot-set.pos-p {
align-items: start;
}
.letter .eventsheet.quarters #roster-and-history .spacer {
display: table-cell;
width: 30%;
}
.letter .eventsheet.quarters #roster-and-history td.position.last-of-group, .letter .eventsheet.quarters #roster-and-history .position.last-of-group {
border-right: none;
}
.letter .eventsheet.quarters #roster-and-history .container {
--padding: 2px;
display: block;
flex: none;
transform: rotate(90deg) translateY(-100%);
transform-origin: top left;
height: calc(4.25in - 2 * var(--page-margin) - 2 * var(--padding));
width: calc(5.5in - 2 * var(--page-margin) - 2 * var(--padding));
padding: var(--padding);
}
.letter .eventsheet.quarters #roster-and-history table {
font-size: 11;
height: 100%;
}
.letter .eventsheet.quarters #roster-and-history table thead tr {
height: inherit;
}
.letter .eventsheet.quarters #roster-and-history table thead tr th {
padding-top: 2px;
padding-bottom: 2px;
}
.letter .eventsheet.quarters #roster-and-history table th.availability-on-day div, .letter .eventsheet.quarters #roster-and-history table th.position div {
transform: none;
text-align: center;
}
.letter .eventsheet.quarters #roster-and-history #defense-pane .slot-set.pos-p {
align-items: start;
}
table tr td.position-capability:not(:empty), #roster-and-history table tr td.position-capability:not(:empty), table tr #roster-and-history .position-capability.position:not(:empty), #roster-and-history table tr .position-capability.position:not(:empty) {
color: var(--color-grey-700);
background-color: var(--color-grey-200);
}
table tr td.position-capability:empty, #roster-and-history table tr td.position-capability:empty, table tr #roster-and-history .position-capability.position:empty, #roster-and-history table tr .position-capability.position:empty {
background-color: white;
}
table tr td.is-present-checkbox, #roster-and-history table tr td.is-present-checkbox, table tr #roster-and-history .is-present-checkbox.position, #roster-and-history table tr .is-present-checkbox.position {
font-size: 0.5em;
text-align: center;
color: white;
/* text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000; */
}
td.is-present-checkbox.available-status-code-0 > span, #roster-and-history .is-present-checkbox.available-status-code-0.position > span {
display: none;
}
td.is-present-checkbox.available-status-code-None > span, #roster-and-history .is-present-checkbox.available-status-code-None.position > span {
display: none;
}
#front-cover {
border: solid 1px black;
}
#front-cover Header {
font-family: "Helvetica Now";
font-weight: 600;
background-color: #323669;
color: white;
display: inline-flex;
border: none;
padding-left: 5px;
padding-right: 5px;
}
#front-cover Header .title {
display: grid;
font-family: "Futura Now";
flex-grow: 1;
align-content: center;
font-size: 14px;
}
#front-cover Header .homeaway {
font-weight: 800;
font-size: xx-large;
}
#front-cover Header .game-number, #front-cover Header .homeaway {
display: grid;
align-content: center;
}
#front-cover Header .game-number {
font-size: large;
font-stretch: extra-condensed;
font-weight: 700;
text-wrap: nowrap;
}
#front-cover > div {
width: inherit;
}
#front-cover th {
background-color: whitesmoke;
}
#front-cover th, #front-cover td, #front-cover #roster-and-history .position, #roster-and-history #front-cover .position {
font-family: "Futura Now";
border: solid 0.5px grey;
}
#front-cover .conjuction {
text-align: center;
font-family: "Futura Now";
text-transform: none;
}
#front-cover .head-to-head {
padding: 5px;
display: flex;
flex-direction: column;
}
#front-cover .opponent, #front-cover .team {
text-align: center;
font-weight: 800;
font-size: xx-large;
align-items: center;
font-family: "Pacifico";
text-transform: none;
display: inline-flex;
width: 100%;
flex-grow: 1;
height: 100%;
}
#front-cover .opponent img, #front-cover .team img {
height: 115px;
}
#front-cover .opponent div:has(.name), #front-cover .team div:has(.name) {
flex-grow: 1;
}
#front-cover .opponent name {
text-align: left;
}
#front-cover .team name {
text-align: right;
}
/*# sourceMappingURL=eventsheet.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/eventsheet.scss"],"names":[],"mappings":";AAAQ;AACA;AACA;AACA;AACA;AACA;AAER;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;AACA;EACE;IACE;;EAEF;IACE;;EAEF;IACE;IACA;;;AAIJ;AACA;EACE;IACE;;EAEF;IACE;;EAEF;IACE;IACA;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;AACA;EAA+B;EAAc;;;AAC7C;EAA+B;EAAc;;;AAC7C;EAA+B;EAAc;;;AAE7C;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAEF;EACE;EACA;;AAEA;EACE;;AAMkB;EAChB;;AAGiB;EACjB;;;AAOR;EACE;;;AAGF;EACE;;;AAGF;EACE;;AACA;EACE;;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;;AAGF;EACE;;AAGF;EACE;;;AAMN;EACI;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;;AAIN;EACE;;;AAGF;EACE;EACE;EACA;EACA;EACA;;AACA;EACE;;;AAIN;EACE;EACE;EACA;EACA;EACA;;AACA;EACE;;;AAIN;EACE;EAEA;EAEA;;AAEA;EACE;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAMN;EACE;EACA;EACA;;AAGF;EAEE;;AAGF;AACE;;AACA;EACE;;AACA;EACE;;AAGJ;EACE;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAKF;EAEA;;AAIA;EACE;EACA;;AAEF;EACE;;AAKF;EACE;;AACA;EACE;;AAGJ;EACE;;AAGF;EACE;;;AAMJ;EACE;;AAEF;EACE;;AACA;EACE;;;AAOJ;EACE;EACA;EACA;EACA,qBACE;;AAIJ;EACE;;AAGF;EACE;;AAGF;AACE;EACA;EACA;EACA;AACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGE;EACA;EACA;EACA;;AAGA;EACE;EACA;EACA;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;;AACA;EACE;;AAQJ;EACE;EACA;EACA;;AAEF;EACE;;;AAON;EACE;;AAGF;EACE;;AAIA;EACE;EACA;;AAMF;EACE;;AAGF;EAOE;EACA;EACA;EACA;;AATA;EACE;;AACA;EACE;;;AAcV;EACE;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;;AAIA;EACE;;AAGF;EACE;EACA;EACA;;AAEE;EACE;;AAEM;EACN;;AAEF;EACA;EACA;EACA;;AAQA;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AA4BR;EACE;;AACA;EACA,SA/BM;;AAoCZ;EACE;EAEA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EAIE;EACA;EACA;;AALA;EACE;;AAKF;EACE;;AACA;EACE;;;AASV;EACE;AACA;EACA;EAEA;EACA;AACA;;AAEA;EACE;EACA;;AAGF;EACE;;AACA;EACE;;;AAMN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAKF;EACE;;AAME;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EAEE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;AAEF;AACE;EACA;EACA;AACA;;AAIA;EACA;;AAKF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAOF;EACE;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACE;;AACA;EACE;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;EAEE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAMN;EACE;EACA;EACA;;AAEF;EACE;;AAIJ;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;;AAGF;EAEE;;AAGF;EACE;EACA;;AAEA;EACE;AACA;EACA;EACA;EACA;;AAQJ;EACE;;AAGF;EACE;;AAGa;EACb;;;AAMF;EACE;;AAEF;EACE;;AAEF;EACE;;AAIA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAQE;EACA;;AARA;EACE;;AACA;EACE;EACA;;AAWJ;EACE;EACA;;AAQJ;EACE;;;AAKN;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;AACA;AAAA;;;AAIF;EACE;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE","file":"eventsheet.css"}

71
src/public/css/lineup.css Normal file
View File

@@ -0,0 +1,71 @@
@import url("/font/bootstrap-icons.css");
:root {
--bc-text-muted: #6c757d;
}
#label_13_5_0 span {
display: none !important;
}
#label_13_5_0:after {
font-family: FontAwesome;
content: "\f005";
}
.lineup-slot .Panel-cell {
display: flex;
align-items: center;
}
.lineup-slot .Panel-cell:has(.drag-handle) {
flex-direction: column;
}
.lineup-slot [class*="availability-status-code"]::before {
padding-right: 6px;
font-family: "bootstrap-icons";
}
.lineup-slot .availability-status-code-1::before {
content: "\F26A";
color: var(--bs-success);
}
.lineup-slot .availability-status-code-2::before {
content: "\F504";
color: var(--bs-primary);
}
.lineup-slot .availability-status-code-0::before {
content: "\F622";
color: var(--bs-danger);
}
.lineup-slot .availability-status-code-null::before {
content: "\F505";
color: var(--bs-secondary);
}
.lineup-slot .lastname {
text-transform: uppercase;
font-weight: bold;
}
.lineup-slot .jerseynumber {
text-transform: uppercase;
font-weight: light;
color: var(--bc-text-muted);
font-size: 0.8em;
}
.lineup-slot .jerseynumber::before {
content: " - ";
}
/* if lineup_entry.availabilityStatusCode == 2
i.bi.bi-question-circle-fill.text-info.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 1
i.bi.bi-check-circle-fill.text-success.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 0
i.bi.bi-x-circle-fill.text-danger.u-spaceRightXs
else
i.bi.bi-question-circle.u-spaceRightXs */

View File

@@ -1,5 +1,31 @@
/* These styles are generated from project.scss. */ /* These styles are generated from project.scss. */
@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");
@import url("../fonts/helvetica-now/stylesheet.css");
header.Header {
background: #323669;
padding: 8px 0;
box-shadow: 0 4px 0 rgba(0, 0, 25, 0.1);
border-bottom: 1px solid #d6d6d6;
color: white;
}
.Header-bannerLogo, .Header-bannerTitle {
margin: 0;
padding: 0;
}
.Header-bannerLogo img {
height: 36px;
width: auto;
}
.Header-bannerTitle {
font-family: "Helvetica", sans-serif;
text-transform: uppercase;
font-weight: bold;
text-align: left;
color: white;
font-size: 28px;
}
.alert-debug { .alert-debug {
color: black; color: black;
background-color: white; background-color: white;
@@ -55,3 +81,17 @@
border-color: #1b73bc; border-color: #1b73bc;
background-color: #1b73bc; background-color: #1b73bc;
} }
.benchcoach-nav {
background-color: #323669;
margin-bottom: 2em;
padding: 0.5em;
color: white;
}
.benchcoach-nav h3 {
font-family: "Helvetica Now";
font-weight: bolder;
color: white;
text-transform: uppercase;
}

View File

@@ -0,0 +1,612 @@
const lineupChangedEvent = new Event('bc:lineupChanged')
document.querySelectorAll('.event-lineup').forEach(lineup=>{
lineup.addEventListener('bc:lineupChanged', (evt)=>{
console.log(`lineup changed`, evt.target)
const lineup = evt.target
colorPositions(lineup)
lineup.querySelectorAll(".lineup-slot").forEach((slot, i) => {
const lineup_segment = determineLineupSegment(slot)
if (lineup_segment != 'bench' && lineup_segment != 'out'){
slot.querySelector("input[name=sequence]").value = i;
} else {
slot.querySelector("input[name=sequence]").value = null;
}
updateFlagInput(slot)
updatePositionInput(slot)
});
}
)
})
document.querySelectorAll('[data-control=popup]').forEach(popup_control=>{
console.log(popup_control)
popup_control.addEventListener('click', (evt)=>{
const popup = evt.target.closest(".Popup")
const to_open = popup.querySelector(".Popup-toggle").dataset.open
popup.querySelectorAll(`[data-popup=${to_open}]`).forEach(popup_container => {
console.log(evt, evt.target, popup, popup_container)
popup_container.classList.toggle('is-open')
evt.stopPropagation()
})
})
})
document.querySelectorAll('.position-label-flags input[type="checkbox"]').forEach(flagCheckbox => {
const lineup = flagCheckbox.closest('.event-lineup')
flagCheckbox.addEventListener('click', ()=>{lineup.dispatchEvent(lineupChangedEvent)})
})
function onPositionSelectChange(elem) {
elem.querySelectorAll("option").forEach((option) => {
if (option.innerText.trim() == elem.value) {
option.setAttribute("selected", "selected");
} else {
option.removeAttribute("selected");
}
});
const lineup = elem.closest('.event-lineup')
lineup.dispatchEvent(lineupChangedEvent)
elem
}
function colorPositions(lineup) {
const class_none = "u-colorNegative"
const class_good = "u-colorPositive"
const class_over = "u-colorHighlight"
lineup.querySelectorAll('.position-status').forEach(
position_status=>{
position_status.classList.remove(class_over, class_good, class_none);
const occurences = lineup.querySelectorAll(`.position-select-box option:checked[value="${position_status.dataset.value}"]`)
switch (occurences.length){
case 0:
position_status.classList.add(class_none)
break;
case 1:
position_status.classList.add(class_good)
break;
default:
position_status.classList.add(class_over)
break;
}
})
}
function initFlagsCheckboxes(){
document.querySelectorAll(".lineup-slot").forEach(lineup_slot=>{
const possible_flags = ['DHd', 'DRd']
const flags_string = lineup_slot.querySelector("input[name=flags]")?.value
const flags = flagSetFromString(flags_string)
possible_flags.forEach(flag=>{
if (flags.has(flag)){
lineup_slot.querySelector(`input[type=checkbox][name=${flag}]`).checked = true
}
})
})
}
const flagSetFromString = (s) => {
if (!s) {return new Set()}
const array = s.split(',').map(item=>item.trim())
return new Set(array)
}
const flagSetFromSlot = (slot) => {
const inputs = slot.querySelectorAll('.position-label-flags input[type=checkbox]:checked')
const set = new Set()
inputs.forEach(i=>set.add(i.name))
return set
}
const flagSetToString = (set) => {
return Array.from(set).join(",");
}
const updateFlagInput = (slot) => {
const flags = flagSetFromSlot(slot)
const lineup_segment = slot.closest('.lineup-segment')
lineup_segment.classList.contains('position-only') ? flags.add('PO') : flags.delete('PO')
slot.querySelector('input[name="flags"]').value = flagSetToString(flags);
}
const updatePositionInput = (slot) => {
const selected_position = slot.querySelector(".position-select-box option:checked");
const lineup_segment = slot.closest('.lineup-segment')
if (selected_position && selected_position.text != "--" && !lineup_segment.classList.contains('bench')) {
slot.querySelector("input[name=label]").value = selected_position.text;
} else {
slot.querySelector("input[name=label]").value = null;
}
}
const determineLineupSegment = (slot) => {
const lineup_segments = ['starting', 'position-only', 'bench', 'out']
const lineup_segment = slot.closest('.lineup-segment')
const classList = Array.from(lineup_segment.classList)
const segments = classList.filter(c=>lineup_segments.includes(c))
if (segments.length == 1) {
return segments[0]
} else {
return ''
}
}
function openAvailabilityReminderModal (el, team_id, event_id) {
const url = `/${team_id}/event/${event_id}/modal-confirm-availability-reminders/`
const form = el.closest('form')
const form_data = new FormData (form)
fetch(url)
.then((response) => {
if (response.ok) {
return response.text();
} else {
return Promise.reject(response.text());
}
})
.then((html) => {
const parser = new DOMParser()
const modal = parser.parseFromString(html, 'text/html')
const modal_node = modal.firstElementChild.querySelector('#modal')
modal_node.classList.add('is-open')
const modal_node_accept = modal.querySelector('Button[data-confirm=yes]')
const checked = Array.from(el.querySelectorAll('input:checked')).map
const body = document.querySelector('body')
body.appendChild(modal_node)
modal_node_accept.addEventListener(
"click", ()=>{
// const memberIds = form_data.getAll('memberId')
const csrf_token = form_data.get('csrfToken')
const selected_status_codes = Array.from(document.querySelectorAll('input:checked')).map(e=>e.value)
const slots = Array.from(document.querySelectorAll('.lineup-slot')).filter(
slot =>{
const slot_status_code = slot.querySelector('input[name=availabilityStatusCode]').value
return selected_status_codes.includes(slot_status_code)
}
)
const memberIds = slots.map(
slot => slot.querySelector('input[name=memberId]').value
)
console.log("sending reminders", el, event_id, memberIds, csrf_token)
sendAvailabilityReminder(el, event_id, memberIds, csrf_token)
body.removeChild(modal_node)
}
)
})
}
function confirmModal(el, prompt, fn, options) {
const url = "/modal-confirm"
const params = new URLSearchParams(prompt)
url.search = params.toString()
fetch(url+"?"+params.toString(), {method:"GET"})
.then((response) => {
if (response.ok) {
return response.text();
} else {
return Promise.reject(response.text());
}
})
.then((html) => {
const parser = new DOMParser()
const modal = parser.parseFromString(html, 'text/html')
const modal_node = modal.firstElementChild.querySelector('#modal')
modal_node.classList.add('is-open')
const modal_node_accept = modal.querySelector('Button[data-confirm=yes]')
const body = document.querySelector('body')
body.appendChild(modal_node)
modal_node_accept.addEventListener("click", ()=>{fn(modal_node, options)})
})
}
const toggleShowAndHideLoading = (el) => {
console.log(el)
el.querySelectorAll('.hideOnLoading').forEach((element)=>{
element.classList.add('u-hidden')
})
el.querySelectorAll('.showOnLoading').forEach((element)=>{
element.classList.remove('u-hidden')
})
}
const completeLoad = (el, success) => {
el.querySelectorAll('.hideOnLoading, .showOnLoading, .showOnFailure, .showOnSuccess').forEach((element)=>{
element.classList.add('u-hidden')
})
if (success) {
el.querySelectorAll('.showOnSuccess').forEach((element) => {
element.classList.remove('u-hidden')
})
} else {
el.querySelectorAll('.showOnFailure').forEach((element) => {
element.classList.remove('u-hidden')
})
}
}
function submitClearLineup(modal, options){
console.log('clearing lineup...')
toggleShowAndHideLoading(modal)
const {team_id, event_id, event_lineup_id} = options
const url = `/${team_id}/event/${event_id}/lineup/${event_lineup_id}/delete`
const form = document.querySelector(`#event-lineup-${event_id} form`);
const data = new FormData(form);
const memberIds = data.getAll('memberId')
console.log(url)
fetch(url, {method:"POST", body: JSON.stringify({memberIds, event_id}), headers: {"Content-Type": "application/json"}})
.then((response) => {
if (response.ok) {
completeLoad(modal, true);
return response.text();
} else {
completeLoad(modal, false);
return Promise.reject(response.text());
}
})
.finally(()=>{
setTimeout(function (){
location.reload()
}, 500)
});//refresh page
}
function submitResetAvailabilities(modal, options){
const {team_id, event_id} = options
toggleShowAndHideLoading(modal)
const url = `/${team_id}/event/${event_id}/reset_availabilities`
const form = document.querySelector(`#event-lineup-${event_id} form`);
const data = new FormData(form);
const memberIds = data.getAll('memberId')
console.log('submitting...', url)
fetch(url, {method:"POST", body: JSON.stringify({memberIds, event_id}), headers: {"Content-Type": "application/json"}})
.then((response) => {
if (response.ok) {
completeLoad(modal, true);
return response.text();
} else {
completeLoad(modal, false);
return Promise.reject(response.text());
}
})
.finally(()=>{
setTimeout(function (){
location.reload()
}, 500)
});//refresh page
}
function emailModal(el, url) {
form = el.closest("form");
console.log(form)
data = new FormData(form);
fetch(url, {
method: "POST",
body: data,
headers: {
'CSRF-Token': data.get('_csrf')
}
})
.then((response) => {
if (response.ok) {
return response.text();
} else {
return Promise.reject(response.text());
}
})
.then((html) => {
const parser = new DOMParser()
const email_modal = parser.parseFromString(html, 'text/html')
const email_modal_node = email_modal.firstElementChild.querySelector('#modal')
email_modal_node.setAttribute('id', `lineup-email-data-${data.get('event_lineup_id')}`)
const body = document.querySelector('body')
email_modal_node.classList.add('is-open')
body.appendChild(email_modal_node)
tinymce.init({
selector:`textarea#email-editor`,
content_css:"/css/application.css",
plugins: 'image',
menubar: false,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | outdent indent | image',
paste_data_images: true,
statusbar:false})
tinymce.remove();
});
}
async function submitEventLineup(form, event) {
event.preventDefault();
console.log(event)
teamsnap_icon = form.querySelector("#teamsnap-icon");
waiting_icon = form.querySelector("#waiting-icon");
success_icon = form.querySelector("#success-icon");
failure_icon = form.querySelector("#failure-icon");
data = new FormData(form);
console.log(form)
url = form.attributes.action.textContent;
toggleShowAndHideLoading(form)
await fetch(url, {
method: "POST",
body: data,
headers: {
'CSRF-Token': data.get('_csrf')
}
})
.then((response) => {
if (response.ok) {
return response.text();
} else {
return Promise.reject(response.text());
}
})
.then((text) => {
event.submitter.blur()
completeLoad(form, true)
console.log(text);
})
.catch((error) => {
event.submitter.blur()
completeLoad(form, false)
console.log(error);
})
.finally(()=>{location.reload()});//refresh page
setTimeout(() => {
[waiting_icon, success_icon, failure_icon].forEach(e=>e.classList.add('u-hidden'))
teamsnap_icon.classList.remove('u-hidden')
}, 3000)
}
async function copyEmailTable (element) {
// range=document.createRange();
// window.getSelection().removeAllRanges();
// // range.selectNode(document.querySelector('.Modal').querySelector('.Modal-body'));
// tinymce.activeEditor.selection.select(tinymce.activeEditor.getBody());
// // window.getSelection().addRange(range);
// document.execCommand('copy');
// window.getSelection().removeAllRanges();
const emailStyle = `
<style>.lineup-email {
font-family: "Helvetica", sans-serif;
}
.lineup-email .title-cell {
font-weight: bold;
background-color: #323669;
color: #fff;
padding: 2px 5px;
text-transform: uppercase;
}
.lineup-email .title-cell.out {
background-color: rgb(244, 199, 195);
color: black;
}
.lineup-email .sequence-cell {
font-weight: bold;
padding: 1px 5px;
text-align: left;
}
.lineup-email .name-cell {
width: 200px;
text-align: left;
}
.lineup-email .position-label-cell {
font-weight: bold;
text-align: right;
}
.Panel .Panel {
border: none;
margin: 0;
}</style>
`
// html_content = emailStyle+tinymce.activeEditor.getContent()
// console.log(html_content)
const table = element.closest('form').querySelector('.lineup-table table')
// navigator.clipboard.write(
// [new ClipboardItem(
// {
// // 'text/plain': new Blob([tinymce.activeEditor.getContent({format: "text"})], {type: 'text/plain'}),
// 'text/plain': new Blob([table.innerText], {type: 'text/plain'}),
// 'text/html': new Blob([emailStyle+table.outerHTML], {type: 'text/html'})
// })
// ])
window.getSelection().removeAllRanges();
var range = document.createRange();
range.selectNode(table);
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
}
moveToLineupSegment = (slot, segment_name) => {
if (!slot.classList.contains('lineup-slot')) {
slot = slot.closest('.lineup-slot')
if (!slot) {return}
}
const current_lineup_segment = slot.closest('.lineup-segment')
if (current_lineup_segment.classList.contains(segment_name)) {
return
}
const lineup = slot.closest('.event-lineup')
const newParent = lineup.querySelector(`.lineup-segment.${segment_name} .slot-set`)
newParent.append(slot)
}
function initSlots () {
document.querySelectorAll('.lineup-slot').forEach(slot=>{
if (slot.dataset.initialLineupSegment) {
moveToLineupSegment(slot, slot.dataset.initialLineupSegment)
slot.removeAttribute('data-initial-lineup-segment')
}
})
}
addToStarting = (el) => {
const slot = el.closest('.lineup-slot')
this.blur()
}
removeToBench = (el) => {
const slot = el.closest('.lineup-slot')
this.blue()
}
function insertLineup(direction, teamId, eventId, element) {
const currentUrl = window.location.href;
let search_params
if (Number(direction) > 0) {
search_params = new URLSearchParams({
page_size:1,
index: 1
})
} else if (Number(direction) < 0) {
search_params = new URLSearchParams({
page_size:1,
index: -1
})
} else {throw new Error("Needs to be a negative number or a positive number")}
fetch(`/${teamId}/event/${eventId}/lineup/adjacent?`+search_params, {
method: "GET"
})
.then((response) => {
if (response.ok) {
return response.text();
} else {
return Promise.reject(response.text());
}
})
.then((html) =>{
const parser = new DOMParser();
const new_lineup_doc = parser.parseFromString(html, 'text/html')
const new_lineup_doc_node = new_lineup_doc.firstElementChild.querySelector('.event-lineup')
const main = document.querySelector("main")
const new_csrf_token = new_lineup_doc.querySelector('form input[name=csrfToken]').value
direction > 0 ? main.appendChild(new_lineup_doc_node) : main.insertBefore(new_lineup_doc_node, element.closest('[id*=event-lineup]'))
main.classList.remove(...main.classList)
main.classList.add('scroll-horizontal', 'u-spaceSidesSm', 'u-flex')
Array.from(document.querySelectorAll(".event-lineup")).forEach((bcLineup) => {
// main.classList.remove('.u-max1200', 'u-flexExpandSides')
bcLineup.classList.remove('u-spaceSidesNone', 'u-sm-spaceSidesAuto')
}
)
Array.from(document.querySelectorAll(".event-lineup .Panel")).forEach((bcLineupPanel) => {
bcLineupPanel.classList.remove('Panel--full')
})
for (input of document.querySelectorAll("form input[name=csrfToken]")){
input.value = new_csrf_token
}
initPage();
})
}
function initPage (){
initSlots();
initFlagsCheckboxes();
for (bcLineup of document.querySelectorAll(".event-lineup")) {
bcLineup.dispatchEvent(lineupChangedEvent)
options = {
animation: 150,
handle: ".Panel-cell:has(.drag-handle), .Panel-cell:has(.sequence)",
ghostClass: "ghost",
group: {
name: bcLineup.id,
put: [bcLineup.id],
pull: [bcLineup.id],
},
onAdd: function (/**Event*/ evt) {
bcLineup.dispatchEvent(lineupChangedEvent)
},
onUpdate: function (/**Event*/ evt) {
bcLineup.dispatchEvent(lineupChangedEvent)
},
};
new Sortable.create(bcLineup.querySelector(".lineup-segment.starting .slot-set"), options);
new Sortable.create(bcLineup.querySelector(".lineup-segment.position-only .slot-set"), options);
new Sortable.create(bcLineup.querySelector(".lineup-segment.bench .slot-set"), {...options, sort:false});
new Sortable.create(bcLineup.querySelector(".lineup-segment.out .slot-set"), {...options, sort:false, group:{...options.group, put:[]}});
}
// for (lineup_slot of document.querySelectorAll(".lineup-segment.out .lineup-slot")) {
// const cells = lineup_slot.querySelectorAll('.Panel-cell:has(.sequence), .Panel-cell:has(.drag-handle), .Panel-cell:has(.position-select-box), button:has(+.position-label-flags)')
// Array.from(cells).forEach(cell=>{
// cell.classList.add('u-hidden')
// })
// }
}
function mailToLink(el, protocol) {
const {to, bcc} = el.dataset
const subject = document.getElementById('email-subject').value
const email_body = document.getElementById('email-editor').value
const url = `${protocol}://compose?recipient=${to}&bcc=${bcc}&subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(email_body)}`
console.log(url)
// location.href=`mailto:${to}${params}`
const windowRef = window.open(url, '_blank');
windowRef.focus();
}
function sendAvailabilityReminder(element, eventId, memberIds, csrf_token) {
const icon = element.querySelector('svg')
const button_text = element.querySelector('span')
icon.classList.toggle('u-hidden')
button_text.classList.toggle('u-hidden')
const loader = '<span class="PulseAnimation"><span class="PulseAnimation-dot"></span><span class="PulseAnimation-dot"></span><span class="PulseAnimation-dot"></span></span>'
const loader_node = new DOMParser().parseFromString(loader, "text/html").firstChild.querySelector('span');
element.appendChild(loader_node)
element.blur();
const data = new FormData();
const url = "../availability_reminders"
data.append('eventId', eventId)
for (var i = 0; i < memberIds.length; i++) {
data.append('memberIds[]', memberIds[i]);
}
console.log(data)
fetch(url, {
method: "POST",
body: data,
headers: {
'CSRF-Token': csrf_token
}
})
.then((response) => {
if (response.ok) {
console.log(response)
return response.text();
} else {
return Promise.reject(response.text());
}
})
.finally(()=>{
loader_node.remove()
icon.classList.toggle('u-hidden')
button_text.classList.toggle('u-hidden')
})
console.log(element, eventId, memberIds)
}
document.addEventListener('DOMContentLoaded', initPage)

31
src/public/js/opponent.js Normal file
View File

@@ -0,0 +1,31 @@
// THIS DOESN'T WORK, CORS ERRORS!!
// const form = document.querySelector("form[name=upload-opponent-logo]")
// form.querySelector("button").addEventListener('click', function() {
// form.requestSubmit();
// })
// form.addEventListener('submit', async function(e) {
// e.preventDefault()
// console.log(e.target)
// data = new FormData(e.target)
// // file = new File(data.file.buffer, data.filename, {
// // type: "image/png",
// // });
// teamsnap.TeamSnap("http://localhost:8080/https://apiv3.teamsnap.com")
// if (teamsnap.hasSession()) {
// const token = sessionStorage.getItem('teamsnap.authToken')
// teamsnap.auth(token);
// teamsnap.loadCollections(async function(err) {
// if (err) {
// console.log(err)
// alert('Error loading TeamSnap SDK');
// return;
// }
// const team_medium = await teamsnap.createTeamMedium(data)
// await teamsnap.uploadTeamMedium(team_medium)
// console.log('Uploaded')
// });
// }
// })

23
src/public/manifest.json Normal file
View File

@@ -0,0 +1,23 @@
{
"short_name": "BenchCoach",
"name": "BenchCoach: An assitant for TeamSnap",
"icons": [
{
"src": "/media/benchcoach.svg",
"type": "image/svg+xml",
"sizes": "800x800"
},
{
"src": "/media/apple-touch-icon.png",
"type": "image/png",
"sizes": "120x120 180x180 167x167 152x152 80x80 120x120 58x58 87x87 76x76 114x114"
}
],
"id": "/",
"start_url": "/",
"background_color": "#323669",
"display": "standalone",
"scope": "/",
"theme_color": "#323669",
"description": "An assitant for TeamSnap"
}

View File

@@ -0,0 +1,3 @@
<svg width="139" height="134" viewBox="0 0 139 134" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path id="Path" d="M 41.293747 133.837494 L 13.012501 112.212502 C 21.93251 102.073471 31.282745 92.321198 41.037498 82.982498 C 44.893745 79.208748 47.327499 76.815002 48.327499 75.802505 C 45.186249 75.316254 36.220001 73.280006 21.434998 69.702499 C 10.762501 67.118752 3.7425 65.207504 0.365002 63.978752 L 12.012501 30.691254 C 28.1175 37.353745 42.595001 44.679993 55.43 52.706245 C 52.435005 32.028748 50.945 15.15126 50.945 2.077515 L 84.220001 2.077515 C 84.220001 11.456238 82.541252 28.460007 79.172501 53.098747 C 81.66375 52.091248 87.087509 49.666245 95.464996 45.791245 C 106.912506 40.678757 117.471252 36.309998 127.139999 32.691254 L 136.790009 66.635002 C 122.783752 69.751251 106.574997 72.809998 88.154999 75.802505 L 110.755005 101.480003 C 115.267502 106.672501 118.842499 110.919998 121.485001 114.205002 L 92.537506 133.175003 L 67.400002 90.792496 C 59.805 104.262497 51.112495 118.615005 41.292503 133.837494"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,11 +1,12 @@
var express = require("express"); const express = require("express");
var passport = require("passport"); const passport = require("passport");
var TeamsnapStrategy = require("passport-teamsnap"); const TeamsnapStrategy = require("passport-teamsnap");
const {teamsnapCallback} = require('../lib/utils')
// const {teamsnap} = require("../app");
// Configure the TeamSnap strategy for use by Passport. // Configure the TeamSnap strategy for use by Passport.
// //
// OAuth 2.0-based strategies require a `verify` function which receives the // OAuth 2.0-based strategies require a `verify` function which receives the
// credential (`accessToken`) for accessing the Facebook API on the user's // credential (`accessToken`) for accessing the TeamSnap API on the user's
// behalf, along with the user's profile. The function must invoke `cb` // behalf, along with the user's profile. The function must invoke `cb`
// with a user object, which will be set at `req.user` in route handlers after // with a user object, which will be set at `req.user` in route handlers after
// authentication. // authentication.
@@ -17,25 +18,21 @@ passport.use(
clientSecret: process.env["TEAMSNAP_CLIENT_SECRET"], clientSecret: process.env["TEAMSNAP_CLIENT_SECRET"],
callbackURL: "/auth/teamsnap/callback", callbackURL: "/auth/teamsnap/callback",
passReqToCallback: true, passReqToCallback: true,
scope: ["read", "write"],
proxy: true
}, },
function (req, accessToken, refreshToken, profile, done) { async function (req, accessToken, refreshToken, profile, done) {
json = JSON.parse(profile._raw); // json = JSON.parse(profile._raw);
new_profile = { access_token: accessToken }; const new_profile = {
new_profile["id"] = json.collection.items[0].data.filter( access_token: accessToken,
(e) => e.name == "id" };
)[0].value; ['id', 'email', 'first_name', 'last_name', 'managed_team_ids'].forEach(
new_profile["email"] = json.collection.items[0].data.filter( k => {
(e) => e.name == "email" new_profile[k] = profile.data[0].get(k)
)[0].value; })
new_profile["first_name"] = json.collection.items[0].data.filter(
(e) => e.name == "first_name"
)[0].value;
console.log("LI#35 session is ", req.session);
console.log("LI#35 session id is ", req.session.id);
req.session.teamsnap_access_token = accessToken; req.session.teamsnap_access_token = accessToken;
teamsnap.init(process.env["TEAMSNAP_CLIENT_ID"]); await initTeamsnap(accessToken)
teamsnap.auth(accessToken);
// teamsnap.enablePersistence();
return done(null, new_profile); return done(null, new_profile);
} }
) )
@@ -52,19 +49,28 @@ passport.use(
// and deserialized. // and deserialized.
passport.serializeUser(function (user, cb) { passport.serializeUser(function (user, cb) {
process.nextTick(function () { process.nextTick(function () {
console.log("L#51 serializing user id", user.id); console.log("L#56 serializing user id", user.id);
cb(null, { cb(null, {
id: user.id, id: user.id,
username: user.email, username: user.email,
name: user.firstName, email: user.email,
first_name: user.first_name,
last_name: user.last_name,
accessToken: user.access_token, accessToken: user.access_token,
managed_team_ids: user.managed_team_ids
}); });
}); });
}); });
passport.deserializeUser(function (user, cb) { passport.deserializeUser(function (user, cb) {
process.nextTick(function () { process.nextTick(async function () {
return cb(null, user); console.log("L#68 deserializing user id", user.id);
try {
await initTeamsnap(user.accessToken)
return cb(null, user);
} catch (err) {
return cb(err)
}
}); });
}); });
@@ -79,7 +85,13 @@ var router = express.Router();
* will be sent to the `GET /login/federated/teamsnap` route. * will be sent to the `GET /login/federated/teamsnap` route.
*/ */
router.get("/login", function (req, res, next) { router.get("/login", function (req, res, next) {
res.render("login"); // https://stackoverflow.com/a/73056806/20522015
returnTo = req.session.returnTo;
if (req.user?.accessToken){
res.redirect(returnTo || "/");
} else {
res.render("login", {layout:"layouts/main"});
}
}); });
/* GET /login/federated/teamsnap /* GET /login/federated/teamsnap
@@ -107,22 +119,43 @@ router.get(
passport.authenticate("teamsnap", function (err, user, info, status) {}) passport.authenticate("teamsnap", function (err, user, info, status) {})
); );
router.get("/auth/teamsnap/callback", function (req, res, next) { router.get(
passport.authenticate("teamsnap", function (err, user, info, status) { "/auth/teamsnap/callback",
if (err) { passport.authenticate("teamsnap", {
// do something with the error successReturnToOrRedirect: "/",
console.error("error: ", err); failureRedirect: "/login",
} keepSessionInfo: true,
// success })
console.log("L#105 user is ", user); );
req.logIn(user, function (err) {
if (err) {
return next(err);
}
return res.redirect("/"); const initTeamsnap = async (accessToken) => {
}); await teamsnap.auth(accessToken);
})(req, res, next); await teamsnap.loadCollections(teamsnapCallback);
await teamsnap.enablePersistence();
}
const ensureLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()){
req.session.returnTo = req.originalUrl
res.redirect("/login");
// return next();
}
else {
req.user = req.session.passport.user
next();
}
}
router.get('/auth/teamsnap/session_storage', (req,res)=>{
res.status(200).json({"teamsnap.authToken":req.user?.accessToken})
}
)
router.post('/logout', function(req, res, next){
req.logout(function(err) {
if (err) { return next(err); }
res.redirect('/');
});
}); });
module.exports = router; module.exports = {router, ensureLoggedIn};

78
src/routes/event.js Normal file
View File

@@ -0,0 +1,78 @@
const express = require("express");
const eventsController = require("../controllers/event");
const router = express.Router();
const tsUtils = require("../lib/utils")
const {teamsnapCallback} = require("../lib/utils")
const multer = require("multer");
const upload = multer()
// Middleware
const loadEvent = (req,res,next) => {
const {team_id, event_id} = req.params;
const bulkLoadTypes = ["event", "availabilitySummary"]
req.promises.push(
teamsnap.bulkLoad(
{teamId: team_id, types: bulkLoadTypes, scopeTo:'event', event__id:event_id},
null,
(err, items) => {teamsnapCallback(err, items, {req, source:"loadEvent", method:'bulkLoad'})}
)
.then(bulkLoadItems=>{
const items = tsUtils.groupTeamsnapItems(bulkLoadItems, bulkLoadTypes);
req.availabilitySummary = items.availabilitySummaries.find(e=>e.eventId==event_id);
req.event = items.events.find(e=>e.id==event_id);
}
))
next();
}
// Middleware
const loadEvents = async (req,res,next) => {
const {team_id, event_id} = req.params
req.timeline = {}
await Promise.all(req.promises)
const {recent_events, upcoming_events} = req
const eventIds = [...recent_events.map(e=>e.id), event_id, ...upcoming_events.map(e=>e.id)]
// if (!req.event_lineup){
bulkLoadTypes = ['event','eventLineup', 'eventLineupEntry']
req.promises.push(
teamsnap.bulkLoad(
{teamId: team_id, types: bulkLoadTypes, scopeTo:'event', event__id:eventIds},
null,
(err, items) => {teamsnapCallback(err, items, {req, source:"loadEvents", method:'bulkLoad'})}
)
.then(items => tsUtils.groupTeamsnapItems(items, bulkLoadTypes))
.then(items => {
req.timeline.events = items.events;
req.timeline.event_lineups = items.eventLineups;
req.timeline.event_lineup_entries = items.eventLineupEntries;
})
)
req.promises.push(
teamsnap.loadAvailabilities(
{eventId: eventIds},
(err, items) => {teamsnapCallback(err, items, {req, source:"loadEvents", method:'loadAvailabilities'})}
).then(availabilities => {
req.timeline.availabilities = availabilities
}
)
)
// }
// else {
// // const {event_lineup} = req
// }
const {event_lineup} = req
next();
}
router.use("/:team_id([0-9]+)/event/:event_id([0-9]+)", loadEvent)
// Routes
router.get("/:team_id([0-9]+)/schedule", eventsController.getEvents);
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)", eventsController.getEvent);
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/availability_reminders", upload.none(), eventsController.sendAvailabilityReminders)
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/modal-confirm-availability-reminders/", eventsController.confirmModalAvailabilityReminders)
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/reset_availabilities",upload.none(), eventsController.submitResetAvailabilities)
module.exports = {router, loadEvent, loadEvents}

63
src/routes/eventlineup.js Normal file
View File

@@ -0,0 +1,63 @@
const express = require("express");
const eventsLineupController = require("../controllers/eventlineup");
const router = express.Router();
const tsUtils = require('../lib/utils')
const multer = require("multer");
const upload = multer()
const { doubleCsrfProtection } = require('../middlewares/csrf');
const {loadRecentAndUpcomingEvents} = require('../middlewares/bulkload')
const {loadEvents} = require('./event')
const {teamsnapCallback} = require("../lib/utils")
// Middleware
const loadEventLineup = (req,res,next) => {
const {team_id, event_id} = req.params
if (!req.event_lineup){
bulkLoadTypes = ['eventLineup', 'eventLineupEntry']
req.promises.push(
teamsnap.bulkLoad(
{teamId: team_id, types: bulkLoadTypes, scopeTo:'event', event__id:event_id},
null,
(err, items) => {teamsnapCallback(err, items, {req, source:"loadEventLineup", method:'bulkLoad'})}
)
.then(items => tsUtils.groupTeamsnapItems(items, bulkLoadTypes))
.then(items => {
req.event_lineup = items.eventLineups.pop();
req.event_lineup_entries = items.eventLineupEntries?.sort((a,b)=>a.sequence-b.sequence) || [];
})
)
req.promises.push(
teamsnap.loadAvailabilities(
{eventId: event_id},
(err, items) => {teamsnapCallback(err, items, {req, source:"loadEventLineup", method:'loadAvailabilities'})}
)
.then(availabilities => req.availabilities = availabilities))
}
else {
// const {event_lineup} = req
}
const {event_lineup} = req
// req.availabilitySummary = items.find((i) => i.type == "availabilitySummary" && i.id == event.id),
// req.event = items.find((i) => i.type == "event" && i.id == event_id)
next();
}
router.use("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup", loadEventLineup)
// Routes
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup", async (req,res) => {
await Promise.all(req.promises);
const {event_lineup} = req
res.redirect(`lineup/${event_lineup.id}`);
}
)
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/adjacent", doubleCsrfProtection, loadRecentAndUpcomingEvents, loadEvents, eventsLineupController.getAdjacentEventLineup);
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)/email", upload.none(), doubleCsrfProtection, eventsLineupController.getEventLineupEmail )
router.get ("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)", upload.none(), doubleCsrfProtection, eventsLineupController.getEventLineup);
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)", upload.none(), doubleCsrfProtection, eventsLineupController.postEventLineup);
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)/delete", upload.none(), eventsLineupController.submitDeleteEventLineupEntries);
module.exports = {router, loadEventLineup}

37
src/routes/eventsheet.js Normal file
View File

@@ -0,0 +1,37 @@
const express = require("express");
const eventsSheetController = require("../controllers/eventsheet");
const {loadEventLineup} = require("./eventlineup");
const {loadEvent, loadEvents} = require("./event");
const {loadRecentAndUpcomingEvents} = require("../middlewares/bulkload")
const router = express.Router();
const tsUtils = require('../lib/utils')
const {teamsnapCallback} = require('../lib/utils')
const multer = require("multer");
const upload = multer()
const linksForEventSheet = async (req, res, next) => {
await Promise.all(req.promises)
const events = [...req.recent_events, req.event, ...req.upcoming_events]
events.forEach((event) => {
console.log()
})
next();
}
router.use("/:team_id([0-9]+)/event/:event_id([0-9]+)/sheet", loadEventLineup)
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)/sheet", loadRecentAndUpcomingEvents, loadEvents, eventsSheetController.getEventSheet)
// Routes
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/sheet", async (req,res) => {
await Promise.all(req.promises);
const {event_lineup} = req
res.redirect(`lineup/${event_lineup.id}/sheet`);
}
)
router.get("/lineup/sheet/blank", eventsSheetController.getEventSheetBlank )
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup/:event_lineup_id([0-9]+)/sheet", upload.none(), eventsSheetController.getEventSheet )
module.exports = {router}

36
src/routes/index.js Normal file
View File

@@ -0,0 +1,36 @@
const express = require("express");
const {ensureLoggedIn} = require("./auth")
var router = express.Router();
var multer = require("multer");
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
const path = require("path")
partials = path.join(__dirname, "../views/partials")
const membersController = require("../controllers/member");
router.use("/", ensureLoggedIn, (req,res,next) => {req.layout="layouts/main";req.promises=[];next();})
router.get("/", (req,res,next) => {
if (!req.session.current_team_id){
res.redirect(`/user/${req.session.passport.user.id}/teams`)
next();
}
else {
res.redirect(`/${req.session.current_team_id}/home`)
next();
}
});
router.get("/:team_id([0-9]+)/members", membersController.getMembers);
router.get("/modal-confirm/", (req,res) => {
const {title, body} = req.query
res.render('modal_confirm', {title, body} )
}
)
module.exports = {router, partials};

6
src/routes/meta.js Normal file
View File

@@ -0,0 +1,6 @@
const express = require("express");
const router = express.Router();
router.get("/favicon.ico", (req, res, next) => res.status(204).end())
module.exports = {router}

42
src/routes/opponent.js Normal file
View File

@@ -0,0 +1,42 @@
const express = require("express");
const opponentsController = require("../controllers/opponent");
const {loadTeam} = require("./team")
var router = express.Router();
const multer = require("multer");
const upload = multer()
const { doubleCsrfProtection } = require('../middlewares/csrf');
const {teamsnapCallback} = require('../lib/utils')
// Middleware
const loadOpponent = (req,res,next) => {
const {opponent_id} = req.params;
const {team} = req
req.promises.push(
teamsnap.loadOpponents(
team.id,
(err, opponents) => {teamsnapCallback(err, opponents, {req, source:"loadOpponent", method:'loadOpponent'})}
)
.then(opponents => {req.opponent=opponents.find(o=>o.id==opponent_id);})
)
req.promises.push(
teamsnap.loadTeamMedia(
team.id,
(err, opponents) => {teamsnapCallback(err, opponents, {req, source:"loadOpponent", method:'teamMedia'})}
)
.then(team_media => {
req.opponent_logo = team_media.find(tm=>tm.description==`opponent-logo-${opponent_id}.png`)
}
)
)
next();
}
router.use("/:team_id([0-9]+)/opponent/:opponent_id([0-9]+)", loadOpponent)
router.get("/:team_id([0-9]+)/opponents", opponentsController.getOpponents);
router.get("/:team_id([0-9]+)/opponent/:opponent_id([0-9]+)", doubleCsrfProtection, opponentsController.getOpponent);
router.post("/:team_id([0-9]+)/opponent/:opponent_id([0-9]+)/upload_logo", upload.single('file'), doubleCsrfProtection, opponentsController.postOpponentLogo);
// router.get("/:team_id([0-9]+)/opponent/:opponent_id/logo", ensureLoggedIn, opponentsController.getOpponentLogo);
module.exports = {router}

48
src/routes/team.js Normal file
View File

@@ -0,0 +1,48 @@
const express = require("express");
const teamsController = require("../controllers/team");
const {loadRecentAndUpcomingEvents} = require("../middlewares/bulkload")
const { load } = require("dotenv");
const router = express.Router();
const tsUtils = require('../lib/utils')
const {teamsnapCallback} = require('../lib/utils')
// Middleware
const loadTeam = async (req,res,next) => {
const {team_id} = req.params;
req.team = await teamsnap.loadTeam(
team_id,
(err, result) => {teamsnapCallback(err, result, {req, source: 'loadTeam', method: 'loadTeam'})}
)
const bulkLoadTypes = ['teamMediaGroup', 'teamPreferences', 'member'];
const items = tsUtils.groupTeamsnapItems(teamsnap.getAllItems(), bulkLoadTypes)
if (req.session.current_team_id == null || req.session.current_team_id != team_id || bulkLoadTypes.filter(t=> !items[t] || items[t].length==0).length > 0){
req.promises.push(teamsnap.bulkLoad(
team_id,
bulkLoadTypes,
(err,items) => {teamsnapCallback(err, items, {req, source: 'loadTeam', method: 'bulkLoad'})}
)
.then(bulkLoadItems=>{
const items = tsUtils.groupTeamsnapItems(bulkLoadItems, bulkLoadTypes)
req.members = items.members;
req.team_media_group = items.teamMediaGroups?.pop();
req.team_preferences = items.teamsPreferences.pop();
req.session.current_team_id = req.team.id;
}
)
)
}
else {
req.members = items.member;
req.team_media_group = items.teamMediaGroup.pop();
req.team_preferences = items.teamPreferences.pop();
}
next();
}
router.use("/:team_id([0-9]+)", loadTeam)
// Routes
router.get('/user/:user_id/teams', teamsController.getTeams)
router.get("/:team_id([0-9]+)/home", loadRecentAndUpcomingEvents, teamsController.getTeamHome);
module.exports = {router, loadTeam}

383
src/scss/application.scss Normal file
View File

@@ -0,0 +1,383 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@import "../../node_modules/@teamsnap/teamsnap-ui/src/css/teamsnap-ui.scss";
@import url('/font/helvetica-now/stylesheet.css');
$color-success: #b7e1cd;
$color-danger: #f4c7c3;
$color-neutral: #acc9fe;
$color-warning: rgb(249, 228, 180);
$color-grey-100: #f8f9fa;
$color-grey-200: #e9ecef;
$color-grey-300: #dee2e6;
$color-grey-400: #ced4da;
$color-grey-500: #adb5bd;
$color-grey-600: #6c757d;
$color-grey-700: #495057;
$color-grey-800: #343a40;
$color-grey-900: #212529;
$row-height: 14px;
$monospace-font: "Inconsolata", monospace;
// Components
@import "components/progress";
:root {
--bc-text-muted:#6c757d;
--bc-avail-color-yes: $cu-positive;
--bc-avail-color-no: $cu-negative;
--bc-avail-color-maybe:#113b63;
}
.availability-bar {
&.going {
background-color: $ts-green;
}
&.not-going {
background-color: $ts-red;
}
&.maybe {
background-color: $ts-blue;
}
&.unknown {
background-color: $ts-grey;
}
}
header {
background: #323669;
padding: 8px 0;
// margin: 0 0 16px 0;
box-shadow: 0 4px 0 rgba(0, 0, 25, 0.1);
border-bottom: 1px solid #d6d6d6;
color: white;
text-decoration: none;
.Header-banner {
display: flex;
justify-content: center;
}
.filler {
flex-grow:1,
}
:has(>.Header-bannerLogo):has(>.Header-bannerTitle) {
display: inline-flex
}
.Header-bannerLogo, .Header-bannerTitle {
margin: 0;
padding: 0;
margin-left: 0.5em;
}
.Header-bannerLogo img {
height: 36px;
width: auto;
}
.Header-bannerTitle {
font-family: "Helvetica", sans-serif;
text-transform: uppercase;
font-weight: bold;
text-align: left;
color: white;
font-size: 28px;
}
}
.btn--Full {
display: block;
width: 100%;
text-align: center;
}
body {
background-color: rgb(246, 246, 246);
}
/* .u-padSm.u-border.u-borderRadiusLg.u-spaceEndsSm.u-maxWidthXs */
.event-card {
font-family: "Open Sans", Helvetica, sans-serif;
border: 1px solid #e6e6e6;
border-radius: 8px;
max-width: 480px;
margin-top: 8px;
margin-bottom: 8px;
background: white;
.title {
margin-bottom: 4px;
}
.event-card-body {
padding: 8px 8px 8px 8px;
}
}
.event-card-body {
> .availability-bar {
margin: 4px;
&.fullwidth {
margin-top: 4px;
margin-bottom: -8px;
margin-left: -8px;
margin-right: -8px;
}
}
.availability-bar.fullwidth .progress {
margin-left: -8px;
margin-right: -8px;
border-radius: 0;
}
}
.event-card {
.date, .location {
color: #7a7a7a;
font-size: 0.9em;
}
.opponent {}
.Button span {
margin-left: 4px;
}
.event-card-footer {
padding: 8px;
border-radius: 0px 0px 8px 8px;
background-color: rgb(251, 251, 251);
border-top: solid 1px rgb(214, 214, 214);
}
}
.event-card-footer div {
text-align: center;
}
a.Panel-row {
color: inherit;
}
.lineup-slot .Panel-cell {
display: inline-flex;
align-items: center;
}
div.event-lineup {
max-width: 576px;
counter-reset: lineup-sequence-counter 0;
margin-left: 8px;
margin-right: 9px;
}
.lineup-slot {
counter-increment: lineup-sequence-counter 1;
.Panel-cell {
&.Panel-cell--header {
background: rgba(256, 256, 256, 0);
}
}
[class*="availability-status-code"]::before {
padding-right: 6px;
font-family: "bootstrap-icons";
}
}
.lineup-slot .availability-status-code-1 .icon {
color: $ts-green;
}
.lineup-slot .availability-status-code-2 .icon {
color: $ts-blue;
}
.lineup-slot .availability-status-code-0 .icon {
color: $ts-red;
}
.lineup-slot {
.availability-status-code-nil .icon, .availability-status-code- .icon {
color: $ts-grey;
}
}
li .availability-status-code- {
content: "\F50B";
color: var(--bs-secondary);
}
.lineup-slot {
line-height: 100%;
vertical-align: middle;
span {
margin:auto;
}
.lastname {
text-transform: uppercase;
font-weight: bold;
}
.jerseynumber {
text-transform: uppercase;
font-weight: light;
color: var(--bc-text-muted);
font-size: 0.8em;
// &::before {
// content: "-";
// margin-right: 4px;
margin-left: 4px;
// }
}
button {
margin-right: 0.5ch;
}
.sequence {
width: 2.4ch;
text-align: right;
margin-right: 0.3ch;
&::before {
content: counter(lineup-sequence-counter);
}
}
.drag-handle {
width: 2ch;
}
.position-select-box {
width: 11ch;
}
.Panel-cell {
&:has(.sequence), &:has(.drag-handle), &:has(.position-select-box) {
flex: 0 0 0% !important;
padding: 0;
}
}
}
div.event-lineup {
.lineup-segment {
&:has(input.Toggle-input:not(:checked)) {
&.out {
.Panel-cell:has(.SelectBox),
.Panel-cell:has(.drag-handle),
button:has(+.position-label-flags),
button.addToStarting,
button.addToBench
{
display: none;
}
}
}
&.bench, &.position-only, &.out {
.Panel-cell:has(.sequence) {
display: none;
}
&.bench button.addToBench {
display: none;
}
}
&.starting button.addToStarting, &.position-only button.addToStarting {
display: none;
}
}
}
.Tooltip:after {
padding: 2px !important;
font-size: inherit !important;
}
@media (max-width: 480px){
.Panel--full {
border-radius: 0;
// margin-right: -16px;
// margin-left: -16px;
border-right: none;
border-left: none;
}}
.lineup-email {
font-family: "Helvetica", sans-serif;
.title-cell {
font-weight:bold;
background-color:#323669;
color:#fff;
padding:2px 5px;
text-transform: uppercase;
&.out {
background-color: rgb(244, 199, 195);
color: black;
}
}
.sequence-cell {
font-weight:bold;
padding: 1px 5px;
text-align: left
}
.name-cell {
width:200px;
text-align: left;
}
.position-label-cell {
font-weight: bold;
text-align: right;
}
}
.Panel .Panel{
// padding: 0;
// border-radius: 0;
// border: none;
// border-top: 1px solid #d6d6d6;
// border-bottom: 1px solid #d6d6d6;
margin: 8px;
}
.scroll-horizontal {
overflow-x: scroll;
}
button:has(+.position-label-flags :checked) {
@extend .Button--blue
}

View File

@@ -0,0 +1,78 @@
// stylelint-disable property-disallowed-list
// Single side border-radius
// Helper function to replace negative values with 0
@function valid-radius($radius) {
$return: ();
@each $value in $radius {
@if type-of($value) == number {
$return: append($return, max($value, 0));
} @else {
$return: append($return, $value);
}
}
@return $return;
}
// scss-docs-start border-radius-mixins
@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {
@if $enable-rounded {
border-radius: valid-radius($radius);
}
@else if $fallback-border-radius != false {
border-radius: $fallback-border-radius;
}
}
@mixin border-top-radius($radius: $border-radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
border-top-right-radius: valid-radius($radius);
}
}
@mixin border-end-radius($radius: $border-radius) {
@if $enable-rounded {
border-top-right-radius: valid-radius($radius);
border-bottom-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-radius($radius: $border-radius) {
@if $enable-rounded {
border-bottom-right-radius: valid-radius($radius);
border-bottom-left-radius: valid-radius($radius);
}
}
@mixin border-start-radius($radius: $border-radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
border-bottom-left-radius: valid-radius($radius);
}
}
@mixin border-top-start-radius($radius: $border-radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
}
}
@mixin border-top-end-radius($radius: $border-radius) {
@if $enable-rounded {
border-top-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-end-radius($radius: $border-radius) {
@if $enable-rounded {
border-bottom-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-start-radius($radius: $border-radius) {
@if $enable-rounded {
border-bottom-left-radius: valid-radius($radius);
}
}
// scss-docs-end border-radius-mixins

View File

@@ -0,0 +1,18 @@
@mixin box-shadow($shadow...) {
@if $enable-shadows {
$result: ();
@each $value in $shadow {
@if $value != null {
$result: append($result, $value, "comma");
}
@if $value == none and length($shadow) > 1 {
@warn "The keyword 'none' must be used as a single argument.";
}
}
@if (length($result) > 0) {
box-shadow: $result;
}
}
}

View File

@@ -0,0 +1,72 @@
$progress-height: 1rem;
$progress-font-size: $tu-base-fontSize * .75;
$progress-bg: $color-grey-200;
$progress-border-radius: $border-radius-small;
$progress-box-shadow: $inset-box-shadow-small;
$progress-bar-color: $ts-white;
$progress-bar-bg: $ts-green;
$progress-bar-animation-timing: 1s linear infinite;
$progress-bar-transition: width .6s ease;
$prefix: "";
// Disable animation if transitions are disabled
// scss-docs-start progress-keyframes
// @if $enable-transitions {
// @keyframes progress-bar-stripes {
// 0% { background-position-x: $progress-height; }
// }
// }
// scss-docs-end progress-keyframes
.progress {
// scss-docs-start progress-css-vars
--#{$prefix}progress-height: #{$progress-height};
// @include rfs($progress-font-size, --#{$prefix}progress-font-size);
--#{$prefix}progress-bg: #{$progress-bg};
--#{$prefix}progress-border-radius: #{$progress-border-radius};
--#{$prefix}progress-box-shadow: #{$progress-box-shadow};
--#{$prefix}progress-bar-color: #{$progress-bar-color};
--#{$prefix}progress-bar-bg: #{$progress-bar-bg};
--#{$prefix}progress-bar-transition: #{$progress-bar-transition};
// scss-docs-end progress-css-vars
display: flex;
height: var(--#{$prefix}progress-height);
overflow: hidden; // force rounded corners by cropping it
// @include font-size(var(--#{$prefix}progress-font-size));
background-color: var(--#{$prefix}progress-bg);
// @include border-radius(var(--#{$prefix}progress-border-radius));
// @include box-shadow(var(--#{$prefix}progress-box-shadow));
}
.progress-bar {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
color: var(--#{$prefix}progress-bar-color);
text-align: center;
white-space: nowrap;
background-color: var(--#{$prefix}progress-bar-bg);
// @include transition(var(--#{$prefix}progress-bar-transition));
}
.progress-bar-striped {
// @include gradient-striped();
background-size: var(--#{$prefix}progress-height) var(--#{$prefix}progress-height);
}
// @if $enable-transitions {
// .progress-bar-animated {
// animation: $progress-bar-animation-timing progress-bar-stripes;
// @if $enable-reduced-motion {
// @media (prefers-reduced-motion: reduce) {
// animation: none;
// }
// }
// }
// }

1027
src/scss/eventsheet.scss Normal file

File diff suppressed because it is too large Load Diff

2
src/views/error.hbs Normal file
View File

@@ -0,0 +1,2 @@
<h1>Oops ...</h1>
<code>{{message}}</code>

120
src/views/event/list.hbs Normal file
View File

@@ -0,0 +1,120 @@
<h1>
Schedule
</h1>
<div class="Panel">
<div class="Panel-body">
<div class="Panel-row Panel-row--withCells u-textDecorationNone">
<div class="Panel-cell u-size7of24 u-textLeft">
<h4 class="Panel-title">
Title
</h4>
</div>
<div class="Panel-cell u-size3of24 u-hidden u-xs-block">
<h4 class="Panel-title">
Date
</h4>
</div>
<div class="Panel-cell u-size3of24 u-hidden u-xs-block">
<h4 class="Panel-title">
Time
</h4>
</div>
<div class="Panel-cell u-size3of24 u-xs-hidden">
<h4 class="Panel-title">
When/Where
</h4>
</div>
<div class="Panel-cell u-size7of24 u-hidden u-xs-block">
<h4 class="Panel-title">
Location
</h4>
</div>
<div class="Panel-cell u-size3of24 u-borderLeft">
<h4 class="Panel-title">
...
</h4>
</div>
</div>
{{#each events as |event|}}
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell u-size7of24 u-textLeft">
<a href="event/{{this.id}}">
{{event.formattedTitle}}
</a>
</div>
<div class="Panel-cell u-size3of24 u-textLeft u-hidden u-xs-block">
{{dateFormat event.startDate "ddd MMM D"}}
</div>
<div class="Panel-cell u-size3of24 u-hidden u-xs-block">
{{dateFormat event.startDate "h:mm A"}}
</div>
<div class="Panel-cell u-size3of24 u-xs-hidden">
{{dateFormat this.startDate "ddd MMM D, h:mm A"}} {{
event.locationName
}}
</div>
<div class="Panel-cell u-size7of24 u-hidden u-xs-block">
{{event.locationName}}
</div>
<div class="Panel-cell u-size3of24 u-borderLeft">
<button
class="Button Button--small Button--default Popup"
onclick='console.log(this);this.querySelector(".Popup-container").classList.toggle("is-open")'
;
>
...
<div
class="Popup-container Popup-container--up Popup-container--right"
>
<div class="Popup-content u-textDecorationNone">
<div class="Grid Grid--fit u-spaceXs">
<div class="Grid-cell u-sizeFit u-spaceXs">
<a href="url_for(event_path :team_id=>team.id, :event_id=>event.id)">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/calendar.svg"}}}</span>
<span class="u-hidden u-xs-inline">Details</span>
</a>
</div>
<div class="Grid-cell u-sizeFit u-spaceXs">
<a href="/{{team.id}}/event/{{event.id}}/lineup">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/clipboard.svg"}}}</span>
<span class="u-hidden u-xs-inline">Lineup</span>
</a>
</div>
<div class="Grid-cell u-sizeFit u-spaceXs">
<a href="/{{team.id}}/event/{{event.id}}/sheet">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/file-earmark.svg"}}}</span>
<span class="u-hidden u-xs-inline">Sheet</span>
</a>
</div>
<div class="Grid-cell u-sizeFit u-spaceXs">
<a href="https://go.teamsnap.com/{{../team.id}}/schedule/view_game/{{event.id}}">
<span>{{{embeddedSvgFromPath "/media/teamsnap_star.svg"}}}</span>
<span class="u-hidden u-xs-inline">TeamSnap</span>
</a>
</div>
</div>
</div>
</div>
</button>
</div>
</div>
{{/each}}
{{!--
a.Button href=url_for(event_path :team_id=>team.id, :event_id=>event.id)
=embedded_svg "teamsnap-ui/assets/icons/schedule svg", class:"Icon"
| Details
.Grid-cell.u-sizeFit.u-spaceXs
a.Button href=url_for(event_lineup_path :team_id => team.id, :event_id => event.id)
=embedded_svg "bootstrap-icons/clipboard svg", class:"Icon"
| Lineup
.Grid-cell.u-sizeFit.u-spaceXs
a.Button href=url_for(event_lineup_card_path :team_id => team.id, :event_id => event.id)
=embedded_svg "bootstrap-icons/book svg", class:"Icon"
| Card
.Grid-cell.u-sizeFit.u-spaceXs
a.Button href="https://go teamsnap com/#{team id}/schedule/view_game/#{event id}"
=embedded_svg "bootstrap-icons/asterisk svg", class:"Icon"
| TeamSnap }} --}}
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="progress">
{{#if availabilitySummary}}
<div class="progress-bar availability-bar going" role="progressbar" style="width:{{availability_percentage availabilitySummary "going"}}%">
{{availabilitySummary.playerGoingCount}}
</div>
<div class="progress-bar availability-bar maybe" role="progressbar" style="width:{{availability_percentage availabilitySummary "maybe"}}%">
{{availabilitySummary.playerMaybeCount}}
</div>
<div class="progress-bar availability-bar not-going" role="progressbar" style="width:{{availability_percentage availabilitySummary "notgoing"}}%">
{{availabilitySummary.playerNotGoingCount}}
</div>
<div class="progress-bar availability-bar unknown" role="progressbar" style="width:{{availability_percentage availabilitySummary "unknown"}}%">
{{availabilitySummary.playerUnknownCount}}
</div>
{{/if}}
</div>

View File

@@ -0,0 +1,34 @@
<div class="Panel">
<div class="Panel-header u-padEndsSm">
<h3><a href="/{{team.id}}/event/{{event.id}}">{{event.formattedTitle}}</a></h3>
</div>
<div class=" Panel-body u-padEndsSm">
<div class=" u-padSidesSm">
<div class="date" >{{dateFormat event.startDate "ddd, MMM D h:mm A" }}</div>
<div class="location">{{event.locationName}}</div>
</div>
<div class=" availability-bar fullwidth">
{{> availability_bar availabilitySummary=event.availabilitySummary}}
</div>
</div>
<div class=" Panel-footer u-flex u-flexJustifyAround u-padSm">
<div class="u-maxWidthXs">
<a class="Button" href="/{{team.id}}/event/{{event.id}}">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/calendar.svg"}}}</span>
<span class="u-hidden">Details</span>
</a>
<a class="Button" href="/{{team.id}}/event/{{event.id}}/lineup">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/clipboard.svg"}}}</span>
<span class="u-hidden">Lineup</span>
</a>
<a class="Button" href="/{{team.id}}/event/{{event.id}}/sheet">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/file-earmark.svg"}}}</span>
<span class="u-hidden">Sheet</span>
</a>
<a class="Button" href="https://go.teamsnap.com/{{team.id}}/schedule/view_game/{{event.id}}">
<span>{{{embeddedSvgFromPath "/media/teamsnap_star.svg"}}}</span>
<span class="u-hidden">TeamSnap</span>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div id="modal" class="Modal Modal--clickableBg">
<div class="Modal-content">
<div onclick="javascript:this.closest('.Modal').remove();">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg" "Modal-iconDismiss"}}}</div>
<div class="Modal-header">
<div class="Modal-title">Send Reminders</div>
</div>
<div class="Modal-body">
<div class="u-padSidesMd">
<strong>Send to players who have selected:</strong>
<div class="u-spaceTopSm"><div class="Checkbox">
<input class="Checkbox-input" type="checkbox" name="undecidedCheckBox" id="undecidedCheckBox" checked="" value>
<label class="Checkbox-label" for="undecidedCheckBox">Undecided</label>
</div>
<div class="Checkbox">
<input class="Checkbox-input" type="checkbox" name="maybeCheckbox" id="maybeCheckbox" value="2">
<label class="Checkbox-label" for="maybeCheckbox">Maybe</label>
</div>
<div class="Checkbox">
<input class="Checkbox-input" type="checkbox" name="attendingCheckbox" id="attendingCheckbox" value="1">
<label class="Checkbox-label" for="attendingCheckbox">Attending</label>
</div>
<div class="Checkbox u-padBottomNone">
<input class="Checkbox-input" type="checkbox" name="notAttendingCheckbox" id="notAttendingCheckbox" value="0">
<label class="Checkbox-label" for="notAttendingCheckbox">Not Attending</label>
</div>
</div>
</div>
</div>
<div class="Modal-footer">
<button class="Button Button--negative" role="button" type="button" onclick="javascript:this.closest('.Modal').remove();" data-confirm="cancel">
Cancel
</button>
<button class="Button Button--primary" role="button" type="button" data-confirm="yes">
Send
</button>
</div>
</div>
</div>

1
src/views/event/show.hbs Normal file
View File

@@ -0,0 +1 @@
{{> event_panel event=event}}

View File

View File

@@ -0,0 +1,132 @@
<div class="u-spaceSidesNone u-sm-spaceSidesAuto event-lineup" id="event-lineup-{{event.id}}" data-event-lineup-id="{{event_lineup.id}}" data-event-id="{{event.id}}">
<form onsubmit="submitEventLineup(this,event)" action="/{{team.id}}/event/{{event.id}}/lineup/{{event_lineup.id}}">
<input type="hidden" name="event_lineup_id" value="{{event_lineup.id}}">
{{!-- <input type="hidden" name="_csrf" value="{{csrfToken}}"> --}}
<input type="hidden" name="csrfToken" value="{{csrfToken}}">
<div class="Panel Panel--full">
<div class="Panel-header u-padEndsSm">
<h3 style="flex: 1 1 0%;">{{event.formattedTitle}}</h3>
<div class="Popup">
<div class="ButtonGroup">
<button class="Button Button--orange" type="submit" formmethod="post">
<div>
<span id="teamsnap-icon" class="hideOnLoading">{{{embeddedSvgFromPath "/media/teamsnap_star.svg"}}}</span>
<span id="waiting-icon" class="u-hidden showOnLoading">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/loader.svg" "Icon--loader"}}}</span>
<span id="success-icon" class="u-hidden showOnSuccess">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/check.svg"}}}</span>
<span id="failure-icon" class="u-hidden showOnFailure">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg"}}}</span>
Save
</div>
</button>
<div class="Button Button--orange .u-padSidesXs Popup-toggle" data-control="popup" data-open="event-lineup-more-actions-{{event_lineup.id}}">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/caret-down.svg"}}}
<div class="Popup-container Popup-container--down Popup-container--right" style="width: 200px" data-popup="event-lineup-more-actions-{{event_lineup.id}}">
<div class="Popup-content u-textDecorationNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="emailModal(this, '{{event_lineup.id}}/email')">
{{{embeddedSvgFromPath "/bootstrap-icons/envelope.svg"}}}
<span>Generate Email</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="/{{team.id}}/event/{{event.id}}/sheet">
<span>{{{embeddedSvgFromPath "/bootstrap-icons/file-earmark.svg"}}}</span>
<span class="u-hidden u-xs-inline">Game Sheet</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="insertLineup(1, {{team.id}}, {{event.id}}, this)">
{{{embeddedSvgFromPath "/bootstrap-icons/caret-right.svg"}}}
<span>Insert next lineup</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="insertLineup(-1, {{team.id}}, {{event.id}}, this)">
{{{embeddedSvgFromPath "/bootstrap-icons/caret-left.svg"}}}
<span>Insert previous lineup</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="openAvailabilityReminderModal(this, {{team.id}}, {{event.id}})">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/send.svg"}}}
<span>Availability Reminders</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="confirmModal(this, {title:'Reset Availabilities',body:'Are sure you want to reset availabilities?'}, submitResetAvailabilities, {team_id:{{team.id}}, event_id:{{event.id}} })";>
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/refresh.svg"}}}
<span>Reset All Availabilities</span>
</a>
<hr class="Divider u-spaceEndsNone">
<a class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="confirmModal(this, {title:'Clear Lineup',body:'Are sure you want to clear lineup?'}, submitClearLineup, {team_id:{{team.id}}, event_id:{{event.id}}, event_lineup_id:{{event_lineup.id}} })">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/trash.svg"}}}
<span>Clear Lineup</span>
</a>
<div class="u-hidden">
<hr class="Divider u-spaceEndsNone">
<span class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="console.log('not implemented yet')">
<span>Publish</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class=" Panel-body u-padEndsSm">
<div class=" u-padSidesSm">
<div class="date">{{dateFormat event.startDate "ddd, MMM D h:mm A" }}</div>
<div class="location">{{event.locationName}}</div>
</div>
<div class="availability-bar fullwidth">
{{> availability_bar availabilitySummary=availabilitySummary}}
</div>
</div>
</div>
<div class="Panel u-maxWidthSm lineup-segment starting Panel--fullWidthMobile Panel--full">
<div class="Panel-body">
<div class="Panel-row Panel-title u-padXs">
<i>{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-check.svg"}}}</i>
<span>Starting Lineup</span>
</div>
<div class=" Panel-row Grid Grid--fit u-textBold u-textCenter u-padXs">
{{#each (positions)}}
<div class="Grid-cell position-status" data-value="{{this}}">{{this}}</div>
{{/each}}
</div>
<div class="slot-set">
</div>
</div>
</div>
<div class="Panel u-maxWidthSm lineup-segment position-only Panel--full">
<div class="Panel-row Panel-title u-padXs">
{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-check.svg"}}}
<span>Position Only</span>
</div>
<div class="slot-set">
</div>
</div>
<div class="Panel u-maxWidthSm lineup-segment bench Panel--full">
<div class="Panel-row Panel-title u-padXs">
{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-minus.svg"}}}
<span>Bench</span>
</div>
<div class="slot-set">
{{#loadSlots}}
{{>slot member=member event_lineup=event_event_lineup availablity=availability}}
{{/loadSlots}}
</div>
</div>
<div class="Panel u-maxWidthSm lineup-segment out Panel--full">
<div class="Panel-row Panel-title u-padXs u-flex">
<div><span style="flex: 1 1 0%;">{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-x.svg"}}}Out</span></div>
<div class="u-flexGrow1"></div>
<div class="Toggle">
<input class="Toggle-input" type="checkbox" id="enable-slots">
<label class="Toggle-label" for="enable-slots"></label>
</div>
</div>
<div class="slot-set">
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,70 @@
<div class="lineup-email">
<div>
<p>Team,</p>
<p></p>
</div>
<div>
<table>
<thead>
<tr>
<th class="title-cell" colSpan=3>
STARTING LINEUP
</th>
</tr>
</thead>
<tbody>
{{#each members}}
{{#if (isInStartingLineup this)}}
<tr>
<td class="sequence-cell">
{{plus1 this.benchcoach.eventLineupEntry.sequence}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell" colSpan=3>Starting (Pos. Only)</th>
</tr>
{{#each members}}
{{#if (isInPositionOnly this)}}
<tr>
<td class="sequence-cell"></td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell" colSpan=3>Subs</th>
</tr>
{{#each members}}
{{#if (isInBench this)}}
<tr>
<td class="sequence-cell">
{{availabilityStatusShort this.benchcoach.availability}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell out" colSpan=3>Out</th>
</tr>
{{#each members}}
{{#if (isInOut this)}}
<tr>
<td class="sequence-cell">
{{availabilityStatusShort this.benchcoach.availability}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<div id="availability-reminder-modal-{{event.id}}" class="Modal Modal--clickableBg">
<div class="Modal-content">
<div onclick="javascript:this.closest('.Modal').remove();">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg" "Modal-iconDismiss"}}}</div>
<div class="Modal-header">
<div class="Modal-title">Send Reminders</div>
</div>
<div class="Modal-body">
</div>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<div id="modal" class="Modal Modal--clickableBg">
<div class="Modal-content">
<div onclick="javascript:this.closest('.Modal').remove();tinymce.remove();">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg" "Modal-iconDismiss"}}}</div>
<div class="Modal-header">
<div class="Modal-title">Email</div>
</div>
<div class="Modal-body">
<form>
<div class="FieldGroup">
<label class="FieldGroup-label">Subject</label>
<input class="Input" id="email-subject" type="text" value="{{dateFormat event.startDate "ddd, MMM D, YYYY h:mm A" }}, {{ event.locationName }}, ({{#if (isAway event) }}@{{/if}}{{ event.opponentName }})">
</div>
<div class="FieldGroup">
<label class="FieldGroup-label">Body</label>
<textarea id="email-editor" class="Input"></textarea>
</div>
<div class="FieldGroup">
<label class="FieldGroup-label">
Lineup
<button class="Button Button--smallSquare" role="button" type="button" onclick="copyEmailTable(this)">
{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-fill.svg"}}}
</button>
</label>
<div class="lineup-email lineup-table">{{>email_table}}</div>
</div>
</form>
</div>
<div class="Modal-footer">
<button class="Button" role="button" type="button" onclick="mailToLink(this, 'readdle-spark');",
data-to="{{user.email}}"
data-bcc="{{joinMemberEmailAddresses (filterNonPlayers members)}}">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/mail.svg"}}}
Spark Mail
</button>
<button class="Button" role="button" type="button" onclick="mailToLink(this, 'mailto');",
data-to="{{user.email}}"
data-bcc="{{joinMemberEmailAddresses (filterNonPlayers members)}}">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/mail.svg"}}}
Mail
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,62 @@
<table class="lineup-table">
<thead>
<tr>
<th class="title-cell" colSpan=3>
STARTING LINEUP
</th>
</tr>
</thead>
<tbody>
{{#each members}}
{{#if (isInStartingLineup this)}}
<tr>
<td class="sequence-cell">
{{plus1 this.benchcoach.eventLineupEntry.sequence}}{{#if (hasPositionFlags this.benchcoach.eventLineupEntry.label)}} {{positionFlags this.benchcoach.eventLineupEntry.label}}{{/if}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{positionLabelWithoutFlags this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell" colSpan=3>Starting (Pos. Only)</th>
</tr>
{{#each members}}
{{#if (isInPositionOnly this)}}
<tr>
<td class="sequence-cell"></td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{positionLabelWithoutPOFlag this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell" colSpan=3>Subs</th>
</tr>
{{#each members}}
{{#if (isInBench this)}}
<tr>
<td class="sequence-cell">
{{availabilityStatusShort this.benchcoach.availability}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
<tr>
<th class="title-cell out" colSpan=3>Out</th>
</tr>
{{#each members}}
{{#if (isInOut this)}}
<tr>
<td class="sequence-cell">
{{availabilityStatusShort this.benchcoach.availability}}
</td>
<td class="name-cell">{{this.lastName}}, {{this.firstName}} #{{this.jerseyNumber}}</td>
<td class="position-label-cell">{{this.benchcoach.eventLineupEntry.label}}</td>
</tr>
{{/if}}
{{/each}}
</tbody>
</table>

View File

@@ -0,0 +1,109 @@
<div class="Panel-expandableRow lineup-slot" data-initial-lineup-segment="{{initial_lineup_segment}}">
<input type="hidden" name="label" value="{{eventLineupEntry.label}}">
<input type="hidden" name="flags" value="{{flagsString eventLineupEntry.flags}}">
<input type="hidden" name="sequence" value="{{eventLineupEntry.sequence}}">
<input type="hidden" name="eventId" value="{{event.id}}">
<input type="hidden" name="eventLineupEntryId" value="{{eventLineupEntry.id}}">
<input type="hidden" name="availabilityStatusCode", value="{{#if availability}}{{availability.statusCode}}{{/if}}">
<input type="hidden" name="memberId" value="{{member.id}}">
<input type="hidden" name="lastName" value="{{member.lastName}}">
<input type="hidden" name="firstName" value="{{member.firstName}}">
<input type="hidden" name="jerseyNumber" value="{{member.jerseyNumber}}">
<input type="hidden" name="emailAddresses" value="{{member.emailAddresses}}">
<div class="Panel-row Panel-row--withCells Panel-row--parent">
<div
class="Panel-cell Panel-cell--header">
<div class="sequence u-textNoWrap u-fontSizeLg"></div>
</div>
<div class="Panel-cell u-padXs u-sizeFill u-flex">
<div
class="Popup availability-status-code-{{
availability?.statusCode
}}"
>
{{#if availability}}
{{#with availability}}
<div class="Popup">
<button class="Popup-toggle Button Button--smallSquare {{avail_status_code_class statusCode}}"
type="button"
data-control="popup"
data-open="availablility-popup-{{eventId}}-{{memberId}}"
>
{{#if notes}}{{{embeddedSvgFromPath "/bootstrap-icons/asterisk.svg"}}}{{else}}{{{avail_status_code_icon statusCode}}}{{/if}}
</button>
<div class="Popup-container Popup-container--left" data-popup="availablility-popup-{{eventId}}-{{memberId}}">
<div class="Popup-content u-padSm u-textCenter">
<h3 class="u-spaceBottomSm">Availability</h3>
{{#if notes}}
<p class="u-textLeft">“ <i>{{notes}}</i> ”</p>
{{else}}
<p class="u-textLeft">No notes.</p>
{{/if}}
<button type="button" class="Button u-spaceTopSm" onclick="sendAvailabilityReminder(this, {{eventId}}, ['{{memberId}}'], {{csrfToken}})">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/send.svg"}}}
<span>Send Reminder</span>
</button>
</div>
</div>
</div>
{{/with}}
{{/if}}
</div>
<div class="u-fontSizeLg u-textNoWrap">
<span class="lastname">
{{member.lastName}}
</span>
<span class="lastname u-hidden u-sm-inline">
, {{member.firstName}}
</span>
<span class="jerseynumber u-hidden u-sm-inline u-fontSizeSm">
#{{member.jerseyNumber}}
</span>
</div>
<div class="u-flexGrow1"></div>
<button type="button" class="Button Button--smallSquare addToBench" onclick="moveToLineupSegment(this, 'bench');this.blur()">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg"}}}
</button>
<button type="button" class="Button Button--smallSquare addToStarting" onclick="moveToLineupSegment(this, 'starting');this.blur()">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/plus.svg"}}}
</button>
<div class="Popup">
<button type="button" class="Popup-toggle Button Button--smallSquare" onclick="this.closest('div').querySelector('.Popup-container').classList.toggle('is-open');this.blur();" href="javascript:void(0)">
{{{embeddedSvgFromPath "/bootstrap-icons/three-dots.svg"}}}
</button>
<div class="Popup-container Popup-container--rightHang position-label-flags">
<div class="Popup-content u-padSm u-textCenter">
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="DRd" id="flag-drd-{{member.id}}-{{eventLineupEntry.id}}">
<label class="Checkbox-label" for="flag-drd-{{member.id}}-{{eventLineupEntry.id}}">DR<small>d</small></label>
</div>
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="DHd" id="flag-dhd-{{member.id}}-{{eventLineupEntry.id}}">
<label class="Checkbox-label" for="flag-dhd-{{member.id}}-{{eventLineupEntry.id}}">DH<small>d</small></label>
</div>
</div>
</div>
</div>
</div>
<div class="Panel-cell u-padXs u-sizeFit">
<div class="SelectBox position-selection">
<select name="positionLabelSelectBox" class="position-select-box SelectBox-options" onchange="onPositionSelectChange(this)" >
<option value="--">
--
</option>
{{#each (positions)}}
<option value="{{this}}" {{#if (comparePositionWithFlags this ../eventLineupEntry)}}selected{{/if}}>
{{this}}
</option>
{{/each}}
</select>
</div>
</div>
<div class="Panel-cell u-padSidesMd u-sizeFit">
<div class="drag-handle">
{{{embeddedSvgFromPath "/bootstrap-icons/grip-vertical.svg"}}}
</div>
</div>
</div>
</div>

View File

View File

@@ -0,0 +1,22 @@
<div class="field-container">
{{{embeddedSvgFromPath "/media/baseball-diamond.svg" 'baseball-diamond'}}}
{{#defenseLineup event_lineup_entries members}}
<div class="slot-set pos-{{this.position}}">
<table class="striped">
<tbody>
<tr class="slot">
<th class="position"></th>
<td class="player-name">{{this.member.lastName}}</td>
</tr>
<tr class="slot substitute">
<th class="position"></th>
<td></td>
</tr>
<tr class="slot substitute">
<th class="position"></th><td></td>
</tr>
</tbody>
</table>
</div>
{{/defenseLineup}}
</div>

View File

@@ -0,0 +1,20 @@
<table>
<tbody>
<div class="slot-set">
{{!-- <% offensive_lineup_entries = by_member.select{|m,d| d[:event_lineup_entry] and d[:event_lineup_entry].label.exclude?("[PO]")}.sort_by{|m,d| d[:event_lineup_entry].sequence}.each_with_index do |(member, d), i| if i < 11%> --}}
{{#offenseLineup 11 event_lineup_entries members}}
<tr class="slot">
<th class="sequence counter" rowspan="2"></th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{positionLabelWithoutFlags this.label}}</td>
</tr>
<tr class="slot substitute">
<td></td>
<td></td>
<td></td>
</tr>
{{/offenseLineup}}
</div>
</tbody>
</table>

View File

@@ -0,0 +1,86 @@
<table>
{{!-- <colgroup><col span="4" class="player"></colgroup> --}}
{{!-- <colgroup><col span="1" class="spacer"></colgroup> --}}
{{!-- <colgroup><col span="1" class="player-stats"></colgroup> --}}
{{!-- <colgroup><col span="4" class="position-capability"></colgroup>
<colgroup><col span="4" class="availability-on-day future"></colgroup>
<colgroup><col span="4" class="availability-on-day past"></colgroup> --}}
<thead>
<tr>
<th colspan="4" id="today-availability">
Available ({{availabilitySummary.playerGoingCount}}|{{availabilitySummary.playerMaybeCount}})
</th>
<th class="spacer first-of-group last-of-group"></th>
<th class="player-stats">
<span class="decimal-point">.</span>AVG
<span class="delimiter">/</span>
<span class="decimal-point">.</span>OBP
<span class="delimiter">/</span>
<span class="decimal-point">.</span>SLG
<span class="delimiter">:</span>PA
</th>
<th class="position-capability pitcher first-of-group">P</th>
<th class="position-capability catcher">C</th>
<th class="position-capability infield">I</th>
<th class="position-capability outfield last-of-group">O</th>
{{!-- <% for timepoint, i in timeline.select{|tp| tp[:comparison_to_selected]>0}.sort{|tp| -tp[:comparison_to_selected]}.each_with_index do%> --}}
{{#loopEvents upcoming_events}}
<th class="availability-on-day avail-today-plus-{{@index}} {{#ifEquals @index 0}}first-of-group{{/ifEquals}}{{#ifEquals @index 3}}last-of-group{{/ifEquals}}" date="{{this.startDate}}"><div>{{dateFormat this.startDate "ddd" }}</div></th>
{{/loopEvents}}
{{#loopEvents recent_events}}
<th class="availability-on-day avail-today-minus-{{@index}} {{#ifEquals @index 0}}first-of-group{{/ifEquals}}{{#ifEquals @index 3}}last-of-group{{/ifEquals}}" date="{{this.startDate}}"><div>{{dateFormat this.startDate "ddd" }}</div></th>
{{/loopEvents}}
</tr>
</thead>
<tbody>
{{!-- <% by_member.select{|m,d| !m.is_non_player}.each_with_index do |(member, d), i|%> --}}
{{#rosterHistory event event_lineup_entries members availabilities}}
<tr class="roster-history-slot{{#if (isStarting this)}} starting-today{{/if}}">
<td class="is-present-checkbox available-status-code-{{this.benchcoach.availability.statusCode}} first-of-group">
<span>■</span>
</td>
<td class="jersey-number available-status-code-{{this.benchcoach.availability.statusCode}}">
{{this.jerseyNumber}}
</td>
<td class="player-name available-status-code-{{this.benchcoach.availability.statusCode}}">
{{this.lastName}}
</td>
<td class="position available-status-code-{{this.benchcoach.availability.statusCode}} last-of-group">
<span>{{this.benchcoach.eventLineupEntry.label}}</span>
</td>
<td class="spacer"></td>
<td class="player-stats first-of-group last-of-group">
<span class="decimal-point">.</span>
<span class="avg">000</span>
<span class="delimiter">/</span>
<span class="decimal-point">.</span>
<span class="obp">000</span>
<span class="delimiter">/</span>
<span class="decimal-point">.</span>
<span class="slg">000</span>
<span class="delimiter">:</span>
<span class="pa">00</span>
</td>
<td class="position-capability pitcher first-of-group">{{positionCapabilityFor this "P"}}</td>
<td class="position-capability catcher">{{positionCapabilityFor this "C"}}</td>
<td class="position-capability infield">{{positionCapabilityFor this "IF"}}</td>
<td class="position-capability outfield last-of-group">{{positionCapabilityFor this "OF"}}</td>
{{#loopEvents ../upcoming_events}}
{{#timepointForMember ../this ../../timeline this}}
<td class="availability-on-day future available-status-code-{{this.availability.statusCode}} {{this.value}} {{#ifEquals @index 0}}first-of-group{{/ifEquals}}{{#ifEquals @index 3}}last-of-group{{/ifEquals}}">
{{this.value}}
</td>
{{/timepointForMember}}
{{/loopEvents}}
{{#loopEvents ../recent_events}}
{{#timepointForMember ../this ../../timeline this}}
<td class="availability-on-day past available-status-code-{{this.availability.statusCode}} {{this.value}} {{#ifEquals @index 0}}first-of-group{{/ifEquals}}{{#ifEquals @index 3}}last-of-group{{/ifEquals}}">
{{this.value}}
</td>
{{/timepointForMember}}
{{/loopEvents}}
</tr>
{{/rosterHistory}}
</tbody>
</table>

View File

@@ -0,0 +1,277 @@
<link rel="stylesheet" href="/css/eventsheet.css">
<body class="{{#if sheet_size}}{{sheet_size}}{{else}}B5{{/if}}">
<div class="sheet eventsheet {{#if sheet_layout}}{{sheet_layout}}{{else}}quarters{{/if}}" id="page-1">
<section class="NE" id="defense-card">
<header>
<div class="event-title float-left">
{{event.formattedTitle}}
</div>
<div class="homeaway float-right">
{{event.gameType}}
</div>
</header>
<div>
<div id="defense-pane">
{{> defense_pane event_lineup_entries=event_lineup_entries members=members}}
</div>
<div class="footer">
<table class="notes">
<tbody>
<tr>
<th>Notes</th>
</tr>
{{#repeat 3}}
<tr>
<td></td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</div>
</section>
<section class="SW" id="roster-and-history">
<div class="container">
{{> roster_and_history
event=event
event_lineup_entries=event_lineup_entries
members=members availabilities=availabilities
recent_events=recent_events
upcoming_events=upcoming_events
}}
</div>
</section>
<section class="NW lineup-card dugout" id="lineup-card-dugout">
<header>
<div class="float-left event-title">{{event.formattedTitle}}</div>
<div class="float-right homeaway">{{event.gameType}}</div>
</header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">Starting</th>
<th class="substitution">Substitution</th>
</tr>
</thead>
<tbody>
{{#offenseLineup 11 event_lineup_entries members}}
<tr class="slot">
<th class="sequence{{#if this.member.lastName}} counter{{/if}}"></th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{this.label}}</td>
<td class="substitution"></td>
</tr>
{{/offenseLineup }}
{{#defenseLineup event_lineup_entries members}}
<tr class="slot">
{{#if (isInPositionOnly this.member)}}{{#if (comparePositionWithFlags "P" this.eventLineupEntry)}}
<th class="sequence">PO</th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{positionLabelWithoutFlags this.eventLineupEntry.label}}</td>
<td class="substitution"></td>
{{/if}}{{/if}}
</tr>
{{/defenseLineup}}
</tbody>
</table>
</div>
</section>
<section class="SE lineup-card exchange" id="lineup-card-exchange">
<header>
<div class="float-left event-title">{{event.formattedTitleForMultiTeam}}</div>
<div class="float-right homeaway">{{event.gameType}}</div>
</header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">Starting</th>
<th class="substitution">Substitution</th>
</tr>
</thead>
<tbody>
{{#offenseLineup 11 event_lineup_entries members}}
<tr class="slot">
<th class="sequence {{#if this.member.lastName}}counter{{/if}}"></th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{this.label}}</td>
<td class="substitution"></td>
</tr>
{{/offenseLineup}}
{{#defenseLineup event_lineup_entries members}}
<tr class="slot">
{{#if (isInPositionOnly this.member)}}{{#if (comparePositionWithFlags "P" this.eventLineupEntry)}}
<th class="sequence">PO</th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{positionLabelWithoutFlags this.eventLineupEntry.label}}</td>
<td class="substitution"></td>
{{/if}}{{/if}}
</tr>
{{/defenseLineup}}
</tbody>
</table>
</div>
</section>
</div>
<div class="sheet eventsheet {{#if sheet_layout}}{{sheet_layout}}{{else}}quarters{{/if}}" id="page-2">
<section class="SE" id="front-cover">
<header>
<div class="game-number">
{{event.label}}
</div>
<div class="title">
<span class="date-time">{{dateFormat event.startDate "ddd, MMM D h:mm A" }}</span>
<span class="location">{{event.locationName}}</span>
</div>
<div class="homeaway">
<span>{{firstLetter event.gameType}}</span>
</div>
</header>
<div style="display:block;max-height: 1em;background-color: lightgray;border-bottom: solid 2px black;">
</div>
<div class="head-to-head">
<div class="team">
<img src="{{team_preferences.links.teamLogo.href}}">
<div>
<span class="name">{{team.name}}</span>
</div>
</div>
{{# if event.opponentName}}
<div class="conjuction">
<span>vs</span>
</div>
<div class="opponent">
<div>
<span class="name">{{event.opponentName}}</span>
</div>
{{#if opponent_logo.mediumUrl }}
<img src="{{opponent_logo.mediumUrl}}">
{{/if}}
</div>
<div class="">
<table>
<thead>
<tr>
<th colspan="2">Final Score</th>
</tr>
</thead>
<tbody>
<tr>
<th>{{team.name}}</th>
<th>{{event.opponentName}}</th>
</tr>
<tr>
<td style="height:5em;width: 50%;"></td>
<td></td>
</tr>
</tbody>
</table>
</div>
{{/if}}
</div>
</section>
<section class="NW blank" id="defense-card">
<header>
<div class="event-title float-left">
&nbsp;
</div>
<div class="homeaway float-right">
&nbsp;
</div>
</header>
<div>
<div id="defense-pane">
{{> defense_pane event_lineup_entries=null members=null}}
</div>
<div class="footer">
<table class="notes">
<tbody>
<tr>
<th>Notes</th>
</tr>
{{#repeat 3}}
<tr>
<td></td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</div>
</section>
<section class="SW lineup-card exchange blank" id="lineup-card-exchange-blank">
<header></header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">
Starting
</th>
<th class="substitution">
Substitution
</th>
</tr>
</thead>
<tbody>
{{#repeat 12}}
<tr class="slot">
<th class="sequence">
</th>
<td class="player-name">
</td>
<td class="jersey-number">
</td>
<td class="position">
</td>
<td class="substitution">
</td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</section>
<section class="NE lineup-card dugout blank" id="lineup-card-dugout-blank">
<header></header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">
Starting
</th>
<th class="substitution">
Substitution
</th>
</tr>
</thead>
<tbody>
{{!-- <% for i in (0...12) do%> --}}
{{#repeat 12}}
<tr class="slot">
<th class="sequence">
</th>
<td class="player-name">
</td>
<td class="jersey-number">
</td>
<td class="position">
</td>
<td class="substitution">
</td>
</tr>
{{/repeat}}
{{!-- <% end %> --}}
</tbody>
</table>
</div>
</section>
</div>
</body>

View File

@@ -0,0 +1,248 @@
<link rel="stylesheet" href="/css/eventsheet.css">
<body class="{{page_size}} ">
<div class="sheet eventsheet {{layout}}" id="page-1">
<section class="NW" id="roster-and-history">
<div class="roster-and-history">
{{> roster_and_history
event=event
event_lineup_entries=event_lineup_entries
members=members availabilities=availabilities
recent_events=recent_events
upcoming_events=upcoming_events
}}
</div>
</section>
<section class="NE blank" id="defense-card">
<header>
<div class="event-title float-left">
</div>
<div class="homeaway float-right">
</div>
</header>
<div>
<div id="defense-pane">
{{> defense_pane event_lineup_entries=event_lineup_entries members=members}}
</div>
<div class="footer">
<table class="notes">
<tbody>
<tr>
<th>Notes</th>
</tr>
{{#repeat 3}}
<tr>
<td></td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</div>
</section>
<section class="SW lineup-card dugout blank" id="lineup-card-dugout">
<header>
<div class="float-left event-title">{{event.formattedTitle}}</div>
<div class="float-right homeaway">{{event.gameType}}</div>
</header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">Starting</th>
<th class="substitution">Substitution</th>
</tr>
</thead>
<tbody>
{{#offenseLineup 11 event_lineup_entries members}}
<tr class="slot">
<th class="sequence{{#if this.member.lastName}} counter{{/if}}"></th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{this.label}}</td>
<td class="substitution"></td>
</tr>
{{/offenseLineup }}
{{#defenseLineup event_lineup_entries members}}
<tr class="slot">
{{#if (isInPositionOnly this.member)}}{{#if (comparePositionWithFlags "P" this.eventLineupEntry)}}
<th class="sequence">PO</th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{positionLabelWithoutFlags this.eventLineupEntry.label}}</td>
<td class="substitution"></td>
{{/if}}{{/if}}
</tr>
{{/defenseLineup}}
</tbody>
</table>
</div>
</section>
<section class="SE lineup-card exchange blank" id="lineup-card-exchange">
<header>
<div class="float-left event-title">{{event.formattedTitle}}</div>
<div class="float-right homeaway">{{event.gameType}}</div>
</header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">Starting</th>
<th class="substitution">Substitution</th>
</tr>
</thead>
<tbody>
{{#offenseLineup 11 event_lineup_entries members}}
<tr class="slot">
<th class="sequence {{#if this.member.lastName}}counter{{/if}}"></th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{this.label}}</td>
<td class="substitution"></td>
</tr>
{{/offenseLineup}}
{{#defenseLineup event_lineup_entries members}}
<tr class="slot">
{{#if (isInPositionOnly this.member)}}{{#if (comparePositionWithFlags "P" this.eventLineupEntry)}}
<th class="sequence">PO</th>
<td class="player-name">{{this.member.lastName}}</td>
<td class="jersey-number">{{this.member.jerseyNumber}}</td>
<td class="position">{{positionLabelWithoutFlags this.eventLineupEntry.label}}</td>
<td class="substitution"></td>
{{/if}}{{/if}}
</tr>
{{/defenseLineup}}
</tbody>
</table>
</div>
</section>
</div>
<div class="sheet eventsheet {{layout}}" id="page-2">
<section class="NW blank" id="defense-card">
<header>
<div class="event-title float-left">
&nbsp;
</div>
<div class="homeaway float-right">
&nbsp;
</div>
</header>
<div>
<div id="defense-pane">
{{> defense_pane event_lineup_entries=null members=null}}
</div>
<div class="footer">
<table class="notes">
<tbody>
<tr>
<th>Notes</th>
</tr>
{{#repeat 3}}
<tr>
<td></td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</div>
</section>
<section class="NE blank" id="defense-card">
<header>
<div class="event-title float-left">
&nbsp;
</div>
<div class="homeaway float-right">
&nbsp;
</div>
</header>
<div>
<div id="defense-pane">
{{> defense_pane event_lineup_entries=null members=null}}
</div>
<div class="footer">
<table class="notes">
<tbody>
<tr>
<th>Notes</th>
</tr>
{{#repeat 3}}
<tr>
<td></td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</div>
</section>
<section class="SW lineup-card exchange blank" id="lineup-card-exchange-blank">
<header></header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">
Starting
</th>
<th class="substitution">
Substitution
</th>
</tr>
</thead>
<tbody>
{{#repeat 12}}
<tr class="slot">
<th class="sequence">
</th>
<td class="player-name">
</td>
<td class="jersey-number">
</td>
<td class="position">
</td>
<td class="substitution">
</td>
</tr>
{{/repeat}}
</tbody>
</table>
</div>
</section>
<section class="SE lineup-card dugout blank" id="lineup-card-dugout-blank">
<header></header>
<div class="starting-lineup-table">
<table>
<thead>
<tr>
<th colspan="4">
Starting
</th>
<th class="substitution">
Substitution
</th>
</tr>
</thead>
<tbody>
{{!-- <% for i in (0...12) do%> --}}
{{#repeat 12}}
<tr class="slot">
<th class="sequence">
</th>
<td class="player-name">
</td>
<td class="jersey-number">
</td>
<td class="position">
</td>
<td class="substitution">
</td>
</tr>
{{/repeat}}
{{!-- <% end %> --}}
</tbody>
</table>
</div>
</section>
</div>
</body>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/css/application.css">
{{#if style}}<link rel="stylesheet" href="/css/{{style}}">{{/if}}
<title>{{#if title}}{{title}}{{else}}BenchCoach{{/if}}</title>
<script>
async function setSession() {
const response = await fetch("/auth/teamsnap/session_storage")
const session_storage = await response.json();
for (const [key, value] of Object.entries(session_storage)) {
window.sessionStorage.setItem(key,value)
}
}
setSession();
</script>
</head>
<body>
<header class="u-spaceBottomMd">
{{> navbar }}
{{{_sections.header}}}
</header>
<main class="u-xs-spaceSidesMd">
{{{ body }}}
</main>
</body>
{{{script_tags scripts}}}
</html>

10
src/views/login.hbs Normal file
View File

@@ -0,0 +1,10 @@
<div class="Panel u-maxWidthXs u-padLg u-spaceSidesAuto">
<h1 class="u-textCenter">Sign in</h1>
<p class="u-spaceEndsMd">Sign into BenchCoach using your TeamSnap account</p>
<a class="Button Button--large Button--orange u-spaceSidesAuto btn--Full" href="/login/federated/teamsnap">
{{{embeddedSvgFromPath "/media/teamsnap_star.svg"}}}
<span>TeamSnap</span>
</a>
</div>

View File

@@ -0,0 +1,26 @@
<div id="modal" class="Modal Modal--clickableBg">
<div class="Modal-content">
<div onclick="javascript:this.closest('.Modal').remove();">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg" "Modal-iconDismiss"}}}</div>
<div class="Modal-header">
<div class="Modal-title">{{title}}</div>
</div>
<div class="Modal-body">
{{body}}
</div>
<div class="Modal-footer">
<button class="Button Button--negative" role="button" type="button" onclick="javascript:this.closest('.Modal').remove();" data-confirm="cancel">
Cancel
</button>
<button class="Button Button--primary" role="button" type="button" data-confirm="yes">
<span class="hideOnLoading">Yes</span>
<span id="success-icon" class="u-hidden showOnSuccess">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/check.svg"}}}</span>
<span id="failure-icon" class="u-hidden showOnFailure">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg"}}}</span>
<span class="PulseAnimation showOnLoading u-hidden">
<span class="PulseAnimation-dot"></span>
<span class="PulseAnimation-dot"></span>
<span class="PulseAnimation-dot"></span>
</span>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<h1>
{{title}}
<hr class="Divider" />
</h1>
<div class="Panel">
<div class="Panel-body">
{{#each opponents}}
<div class="Panel-row Panel-row--withCells u-textDecorationNone">
<a class="Panel-cell" href="opponent/{{this.id}}">
{{this.name}}
</a>
</div>
{{/each}}
</div>
</div>

View File

@@ -0,0 +1,79 @@
<div class="Panel">
<div class="Panel-header">
<div class="Panel-title">
{{opponent.name}}
</div>
</div>
<div class="Panel-body">
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell Panel-cell--header">
ID
</div>
<div class="Panel-cell">
{{opponent.id}}
</div>
</div>
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell Panel-cell--header">
Contact Name
</div>
<div class="Panel-cell">
{{opponent.contactsName}}
</div>
</div>
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell Panel-cell--header">
Contact Phone
</div>
<div class="Panel-cell">
{{opponent.contactsPhone}}
</div>
</div>
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell Panel-cell--header">
Contact Email
</div>
<div class="Panel-cell">
{{opponent.contactsEmail}}
</div>
</div>
<div class="Panel-row Panel-row--withCells">
<div class="Panel-cell Panel-cell--header">
Logo
</div>
<div class="Panel-cell">
{{#if opponent_logo}}
<img src="{{opponent_logo.mediumUrl}}" width="64" height="64" />
{{else}}
<form name="upload-opponent-logo" action="{{opponent.id}}/upload_logo" method="post" enctype="multipart/form-data" hidden>
{{!-- THIS DOESN'T WORK XHR2 ERRORS (FORMDATA) --}}
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="csrfToken" value="{{csrfToken}}">
<input type="hidden" name="teamMediaGroupId" value="{{team_media_group.id}}">
<input type="hidden" name="memberId" value="{{member.id}}">
<input type="hidden" name="teamId" value="{{team.id}}">
<input type="hidden" name="description" value="team-logo-{{opponent.id}}.png">
<div class="FieldGroup">
<label for='file' class="FieldGroup-label">Select files</label>
<input id='file' name='file' type="file">
<button class="submit-btn Button" type="submit">Upload</button>
</div>
</form>
<div
class="Button"
onclick="navigator.clipboard.writeText('opponent-logo-{{
opponent.id
}}.png');window.open('https://go.teamsnap.com/{{team.id}}/files/list/{{team_media_group.id}}');"
>
<img class="Icon" src="/bootstrap-icons/clipboard.svg" />
<span>
Copy Filename and Go to Teamsnap
</span>
</div>
{{/if}}
</div>
</div>
</div>
</div>
<script src="/js/teamsnap.min.js"></script>
<script src="/js/opponent.js"></script>

View File

@@ -0,0 +1,39 @@
<div class="Header-banner">
<a href="/" class="">
<div class="Header-bannerLogo">
<img class="logo" src="/media/benchcoach.svg" alt="BenchCoach Logo">
</div>
<div class="Header-bannerTitle">
BenchCoach
</div>
</a>
{{#if user}}
<div class="filler"></div>
<div class="u-padSidesSm u-spaceAuto">
<div class="Popup">
<div class="Button Button--small Popup-toggle" onclick="this.closest('.Popup').querySelector('.Popup-container').classList.toggle('is-open')">
Account
</div>
<div class="Popup-container Popup-container--down Popup-container--right u-sizeFit">
<div class="Popup-content u-padXs u-sizeFit u-fontSizeSm">
<h6 class="h6 title u-textNoWrap u-fontSizeSm u-textSemiBold">{{user.first_name}} {{user.last_name}}</h6>
<div class="u-textNoWrap u-fontSizeSm">{{user.email}}</div>
<hr class="Divider u-spaceEndsNone">
<div class="u-padBottomSm u-padTopSm">
<a href="/user/{{user.id}}/teams" class="u-spaceBottomSm Button Button--small">
<span>{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/team.svg"}}}</span>
Teams
</a>
<form method="post" action="/logout">
<button type="submit" name="logout" class="u-spaceBottomSm Button Button--small">
Logout
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{/if}}
</div>

51
src/views/team/home.hbs Normal file
View File

@@ -0,0 +1,51 @@
<div class=" Grid u-spaceBottomSm">
<div class=" Grid-cell u-spaceNone u-sizeFit">
<img src="{{team_preferences.links.teamLogo.href}}", style="height: 64px;">
</div>
<div class=" Grid-cell u-sizeFill u-spaceNone">
<h2 class=" u-spaceNone">{{team.name}}</h2>
<p class=" u-spaceNone">{{team.season_name}}</p>
<small class=" u-spaceNone"><a href="{{team.leagueUrl}}">{{team.leagueName}}</a></small>
</div>
</div>
<div class=" Grid-cell u-sizeFit u-padTopSm u-padBottomSm">
<div class=" u-flex u-flexAlignItemsCenter u-flexJustifyEnd">
<div class="ButtonGroup">
<a class="Button" href="schedule">
<img src ="/teamsnap-ui/assets/icons/schedule.svg" class="Icon">
<span class="span u-hidden u-xs-inline">Schedule</span>
</a>
<a class="Button u-hidden" href="members">
<img src ="/teamsnap-ui/assets/icons/roster.svg" class="Icon">
<span class="span u-hidden u-xs-inline">Roster</span>
</a>
<a class="Button" href="opponents">
<img src ="/teamsnap-ui/assets/icons/team.svg" class="Icon">
<span class="span u-hidden u-xs-inline">Opponents</span>
</a>
</div>
</div>
</div>
<h2 class=" ">Upcoming Events</h2>
<hr class="Divider" />
<div class="Grid Grid--withGutter">
{{#each upcoming_events}}
<div class="Grid-cell u-xs-size1of2 u-sm-size1of3">
{{>event_panel event=this}}
</div>
{{/each}}
</div>
<h2 class="">Recent Events</h2>
<hr class="Divider" />
<div class="Grid Grid--withGutter">
{{#each recent_events}}
<div class="Grid-cell u-xs-size1of2 u-sm-size1of3">
{{>event_panel event=this}}
</div>
{{/each}}
</div>

49
src/views/team/list.hbs Normal file
View File

@@ -0,0 +1,49 @@
<div class=" Panel u-sm-size1of2 u-spaceSidesAuto u-spaceSm u-maxWidthSm">
<div class="Panel-header">
<h3 class="Panel-title">
My Teams
</h3>
</div>
<div class="Panel-body">
{{#each teams}}
{{#unless this.isArchivedSeason}}
<a
class="Panel-row Panel-row--withCells u-textDecorationNone"
href="/{{this.id}}/home"
>
<div class=" Panel-cell u-flexGrow1 u-textNoWrap">
<b>
{{this.name}}
</b>
</div>
<div class=" Panel-cell u-fontSizeSm u-size1of3 u-textRight">
{{this.seasonName}}
</div>
</a>
{{/unless}}
{{/each}}
<div class="Panel-header">
<h3 class="Panel-title">
Archived Seasons
</h3>
</div>
{{#each teams}}
{{#if this.isArchivedSeason}}
<a
class="Panel-row Panel-row--withCells u-textDecorationNone"
href="/{{this.id}}/home"
>
<div class=" Panel-cell u-flexGrow1 u-textNoWrap">
<b>
{{this.name}}
</b>
</div>
<div class=" Panel-cell u-fontSizeSm u-size1of3 u-textRight">
{{this.seasonName}}
</div>
</a>
{{/if}}
{{/each}}
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
html
head
body
h1 error
h2
error.status
pre
message #{message}

View File

@@ -1,46 +0,0 @@
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title #{event.formattedTitle}
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/font/bootstrap-icons.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body
.container
.Panel
.Panel-header
h3.Panel-title #{event.formattedTitle}
.Panel-body
.Panel-row
h6.card-text.text-muted.mb-2
|#{event.startDate}
br
|#{event.locationName}
.Panel-row
h4 Availability
.progress
div(class="progress-bar bg-success fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerGoingCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerGoingCount}
div(class="progress-bar bg-info fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerMaybeCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerMaybeCount}
div(class="progress-bar bg-danger fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerNotGoingCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerNotGoingCount}
div(class="progress-bar text-secondary fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerUnknownCount/team.playerMemberCount)*100).toString() + "%"};
background-color: var(--bs-gray-200)`)
|#{availabilitySummary.playerUnknownCount}
hr
div.d-flex
a(class="Button m-auto" href=`/${team_id}/event/${event.id}/lineup`)
i(class="bi bi-clipboard")
span.mx-1 Lineup
a(class="Button m-auto" href=`/${team_id}/event/${event.id}/gamecard`)
i(class="bi bi-book")
span.mx-1 Game Card
a(class="Button m-auto" href=`https://go.teamsnap.com/${team_id}/schedule/view_game/${event.id}`)
i(class="bi bi-asterisk")
span.mx-1 TeamSnap

View File

@@ -1,37 +0,0 @@
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - Teams
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body
.container
.Panel
.Panel-header
.Panel-title Schedule
.Panel-body
each event in events
- var availabilitySummary = availabilitySummaries.find((a)=>a.eventId==event.id)
.Panel-row
a(class="event list-group-item" href=`/${team_id}/event/${event.id}`)
h4 #{event.formattedTitle}
p.small
| #{event.startDate.toLocaleDateString("en-us",{weekday: "short", day: "numeric",month: "short"})}
| #{event.startDate.toLocaleTimeString("en-us",{hour: "numeric", minute: "2-digit"})}
p #{event.locationName}
.progress
div(class="progress-bar bg-success fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerGoingCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerGoingCount}
div(class="progress-bar bg-info fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerMaybeCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerMaybeCount}
div(class="progress-bar bg-danger fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerNotGoingCount/team.playerMemberCount)*100).toString() + "%"}`)
|#{availabilitySummary.playerNotGoingCount}
div(class="progress-bar text-secondary fw-bold" role="progressbar" style=`
width: ${((availabilitySummary.playerUnknownCount/team.playerMemberCount)*100).toString() + "%"};
background-color: var(--bs-gray-200)`)
|#{availabilitySummary.playerUnknownCount}

View File

@@ -1,409 +0,0 @@
html
head
meta(charset='utf-8')
title #{event.formattedTitle}
link(rel='stylesheet' href='/css/gamecard.css')
body(class="B5")
input(name="team_id", type="hidden" value=`${team_id}`)
input(name="event_id", type="hidden" value=`${event_id}`)
#page-1.sheet.gamecard
section#todays-game
.grid-container
.section-header
#todays-game-header.bar-left.event-title
| #{event.formattedTitle}
| #{event.startDate.toLocaleDateString("en-us",{weekday: "short", day: "numeric",month: "short"})}
| #{event.startDate.toLocaleTimeString("en-us",{hour: "numeric", minute: "2-digit"})}
.bar-right.homeaway #{event.gameType}
.bar-span.gametitle
#offense-pane.left
table#starting-lineup-offense
tbody
each _, i in Array(11)
- if (typeof(event_lineup_entries_offense[i]) !== 'undefined'){
tr
th(rowspan='2') #{i+1}
td(id=`offense-slot-${i}-name` class="player-name") #{event_lineup_entries_offense[i].member.lastName}
td(id=`offense-slot-${i}-jersey-number` class="jersey-number") #{event_lineup_entries_offense[i].member.jerseyNumber}
td(id=`offense-slot-${i}-position` class="position") #{event_lineup_entries_offense[i].label}
tr.substitute
td
td
td
- } else {
tr
th(rowspan='2')
td(id=`offense-slot-${i}-name` class="player-name")
td(id=`offense-slot-${i}-jersey-number` class="jersey-number")
td(id=`offense-slot-${i}-position` class="position")
tr.substitute
td
td
td
- }
#defense-pane.right
.container
.field-container
image(src='/media/baseball-diamond.svg')
.row(style='justify-content: center')
.defense-slot-set
table
tr
th.position CF
td#defense-slot-CF-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("CF")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.row(style='justify-content: space-between')
.defense-slot-set
table
tr
th.position LF
td#defense-slot-LF-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("LF")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.defense-slot-set
table
tr
th.position RF
td#defense-slot-RF-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("RF")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.row(style='justify-content: space-around')
.defense-slot-set
table
tr
th.position SS
td#defense-slot-SS-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("SS")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.defense-slot-set
table
tr
th.position 2B
td#defense-slot-2B-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("2B")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.row(style='justify-content: space-between')
.defense-slot-set
table
tr
th.position 3B
td#defense-slot-3B-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("3B")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.defense-slot-set
table
tr
th.position 1B
td#defense-slot-1B-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("1B")) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.row(style='justify-content: center')
.defense-slot-set
table
tr
th.position C
td#defense-slot-C-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("C") && !lue.label.startsWith("CF") ) || {"member":{}}).member.lastName}
tr
td(colspan='2')
tr
td(colspan='2')
.pitching-container
.defense-slot-set
table
tr
th.position P
td#defense-slot-P-name.player-name
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("P")) || {"member":{}}).member.lastName}
td.jersey-number
| #{(event_lineup_entries.find((lue)=>lue.label.startsWith("P")) || {"member":{}}).member.jerseyNumber}
td.position
tr
th.position RP
td#defense-slot-RP1-name.player-name
td
td
tr
th.position RP
td#defense-slot-RP2-name.player-name
td
td
.footer
table
tr
th Notes
td
tr
td
tr
td
section#roster-and-history
div
table
thead
tr
th#today-availability(colspan='3') Available (
| #{availabilitySummaries.find((e)=>e.id==event_id).playerGoingCount}|
| #{availabilitySummaries.find((e)=>e.id==event_id).playerMaybeCount}
| )
th.player-stats
span.decimal-point .
| AVG
span.delimiter /
span.decimal-point .
| OBP
span.delimiter /
span.decimal-point .
| SLG
span.delimiter :
| PA
th.position-capability.pitcher P
th.position-capability.catcher C
th.position-capability.infield I
th.position-capability.outfield O
each event_future, i in events_future
th(id=`avail-header-today-plus-${i+1}` class="availability future")
.rotate #{event_future.startDate.toLocaleDateString("en-us", {weekday: "short"})}
each event_past, i in events_past
th(id=`avail-header-today-minus-${i+1}` class="availability past")
.rotate #{event_past.startDate.toLocaleDateString("en-us", {weekday: "short"})}
tbody
each row, index in availabilities.filter((e)=>e.event.id==event_id && !e.member.isNonPlayer)
tr(id=`roster-history-slot-${index+1}` class=``)
td(class=`is-present-checkbox available-status-code-${row.statusCode}`)
span &#x25A0;
td(
class=`
jersey-number
border-left
available-status-code-${row.statusCode}
${event_lineup_entries.find((lue)=>lue.member.id==row.member.id) !== undefined ? "starting" : ""}
`)
| #{row.member.jerseyNumber}
td(
class=`
player-name
available-status-code-${row.statusCode}
${event_lineup_entries.find((lue)=>lue.member.id==row.member.id) !== undefined ? "starting" : ""}
`)
| #{row.member.lastName}
td.player-stats.border-left.border-right
span.decimal-point .
span.avg 000
span.delimiter /
span.decimal-point .
span.obp 000
span.delimiter /
span.decimal-point .
span.slg 000
span.delimiter :
span.pa 00
td.position-capability.pitcher #{row.member.position.includes("P") ? "\u2713" : ""}
td.position-capability.catcher #{row.member.position.includes("C") ? "\u2713" : ""}
td.position-capability.infield #{row.member.position.includes("IF") ? "\u2713" : ""}
td.position-capability.outfield #{row.member.position.includes("OF") ? "\u2713" : ""}
- var future_availability
- var future_lineupEntry
each future_event, i in events_future
- future_availability = availabilities.find((el)=>el.eventId ==future_event.id && el.memberId==row.member.id)
- future_lineupEntry = all_lineup_entries.find((el)=>el.eventId ==future_event.id && el.member.id==row.member.id)
- console.log(future_availability)
td(id=`avail-${row.member}-today-plus-${i+1}` class=`
row
future
availability
available-status-code-${future_availability.statusCode}
`)
if future_lineupEntry
|#{future_lineupEntry.label.slice(0,2)}
else
|#{future_availability.status[0]}
- var past_availability
- var past_lineupEntry
each past_event, i in events_past
- past_availability = availabilities.find((el)=>el.eventId==past_event.id && el.memberId==row.memberId)
- past_lineupEntry = all_lineup_entries.find((el)=>el.event.id==past_event.id && el.member.id==row.member.id)
td(id=`avail-${row.member}-today-minus-${i+1}` class=`
row
past
availability
available-status-code-${past_availability.statusCode}
${past_lineupEntry ? "started" : ""}
`)
if past_lineupEntry
|#{past_lineupEntry.label.slice(0,2)}
else
|#{past_availability.status[0]}
tfoot
tr
th(colspan='3')
th
th(colspan='4')
each event_future, i in events_future
th(class=`availability future`)
.rotate #{availabilitySummaries.find((el)=>el.eventId == event_future.id).playerGoingCount}
th.today-minus-1
.rotate
th.today-minus-2
.rotate
th.today-minus-3
.rotate
th.today-minus-4
.rotate
section#lineup-card-dugout.lineup-card
.grid-container
.section-header
.bar-left.event-title
| #{event.formattedTitle}
.bar-right.homeaway #{event.gameType}
.starting-lineup-table
table
thead
tr
th(colspan='4') Starting
tbody
each i in [0,1,2,3,4,5,6,7,8,9,10]
- if (typeof(event_lineup_entries_offense[i]) !== 'undefined'){
tr
th.sequence.label #{event_lineup_entries_offense[i].sequence +1}
td.player-name #{event_lineup_entries_offense[i].member.lastName}
td.jersey-number #{event_lineup_entries_offense[i].member.jerseyNumber}
td.position #{event_lineup_entries_offense[i].label}
- } else {
tr
th.sequence.label
td.player-name
td.jersey-number
td.position
tr
td
td
td
td
- }
.substitution-table
table(style='width: 100%')
thead
tr
th Substitution
tbody
each i in [0,1,2,3,4,5,6,7,8,9,10,11]
tr
td.substitution
tr
td.substitution
section#lineup-card-exchange.lineup-card
.grid-container
.section-header.event-title #{event.formattedTitleForMultiTeam}
.starting-lineup-table
table.starting-lineup-table
thead
tr
th
th.player-name Name
th.jersey-number Num
th.position Pos
tbody
each _,i in Array(10)
- if (typeof(event_lineup_entries_offense[i]) !== 'undefined'){
tr
th.sequence.label #{event_lineup_entries_offense[i].sequence+1}
td.player-name #{event_lineup_entries_offense[i].member.lastName}
td.jersey-number #{event_lineup_entries_offense[i].member.jerseyNumber}
td.position #{event_lineup_entries_offense[i].label}
- } else {
tr
th.sequence.label
td.player-name
td.jersey-number
td.position
tr
td
td
td
td
- }
#page-2.sheet.gamecard
section#back-cover
section#front-cover
div.grid-container
.section-header
.bar-right.homeaway #{event.gameType}
.event-title
| #{event.startDate.toLocaleDateString("en-us",{weekday: "long", day: "numeric",month: "short"})},
| #{event.startDate.toLocaleTimeString("en-us",{hour: "numeric", minute: "2-digit"})}
br
| #{event.locationName}
div.team
|#{event.team.name}
div.opponent
|#{event.opponent.name}
section#lineup-card-dugout-empty.lineup-card
.grid-container
.section-header
.starting-lineup-table
table
thead
tr
th(colspan='4') Starting
tbody
each _ in Array(12)
tr
th.sequence.label
td.player-name
td.jersey-number
td.position
.substitution-table
table(style='width: 100%')
thead
tr
th Substitution
tbody
each _ in Array(11)
tr
td.substitution
tr
td.substitution
section#lineup-card-exchange-empty.lineup-card
.grid-container
.section-header
.starting-lineup-table
table.starting-lineup-table
thead
tr
th
th.player-name Name
th.jersey-number Num
th.position Pos
tbody
each _ in Array(12)
tr
th.sequence.label
td.player-name
td.jersey-number
td.position

View File

@@ -1,29 +0,0 @@
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - Home
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/css/project.css')
body.bg-light
.container
.row
.text-center.my-2
.row
h1
img.mx-auto(src="media/benchcoach.svg" style="width: 2.5em;")
.row
h1
strong
| Welcome to
span.text-nowrap BenchCoach
.text-center.lead.fst-italic.fw-light
| An assistant coach for TeamSnap
.row
.col.text-center
if req.user
ul.list-group
each team in teams
a(class='team list-group-item' href=`/${team.id}`) #{team.name} [#{team.seasonName}]
else
a.btn.btn-outline-primary(href="login")
| Login

View File

@@ -1,139 +0,0 @@
html
head
meta(charset='utf-8')
title #{event.formattedTitle}
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/font/bootstrap-icons.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body.bg-light
.container
div(style="max-width: 455px")
.Panel
.panel-header
.Panel-title #{event.formattedTitle}
.Panel-body
.Panel-row
p.text-muted.mb-2 #{event.startDate}
p #{event.locationName}
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard-check.me-1
span Starting Lineup
.Panel-row.Grid.Grid--fit.fw-bold.text-center.u-padXs
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
if event_lineup_entries.map((lue)=>lue.label).includes(pos)
.Grid-cell.text-success
|#{pos}
else
.Grid-cell.text-danger
|#{pos}
each lineup_entry, i in event_lineup_entries_offense
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header.u-padXs.u-size1of12
.u-flexAlignSelfCenter
|#{i+1}
.Panel-cell.u-padXs.u-size8of12.fw-bold.text-uppercase
if lineup_entry.availabilityStatusCode == 2
i.bi.bi-question-circle-fill.text-info.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 1
i.bi.bi-check-circle-fill.text-success.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 0
i.bi.bi-x-circle-fill.text-danger.u-spaceRightXs
else
i.bi.bi-question-circle.u-spaceRightXs
|#{lineup_entry.member.lastName}
.Panel-cell.u-padXs.u-size2of12
.SelectBox
select.SelectBox-options
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
option(selected=lineup_entry.label==pos) #{pos}
.Panel-cell.u-padXs.u-flexAlignSelfCenter.u-size1of12
.drag-handle
i.bi.bi-grip-vertical.text-secondary
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard-minus.me-1
span Starting Lineup (Position Only)
each lineup_entry, i in event_lineup_entries
if lineup_entry.label.includes("[PO]")
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header.u-padXs.u-size1of12
.u-flexAlignSelfCenter
|#{i+1}
.Panel-cell.u-padXs.u-size8of12.fw-bold.text-uppercase
if lineup_entry.availabilityStatusCode == 2
i.bi.bi-question-circle-fill.text-info.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 1
i.bi.bi-check-circle-fill.text-success.u-spaceRightXs
else if lineup_entry.availabilityStatusCode == 0
i.bi.bi-x-circle-fill.text-danger.u-spaceRightXs
else
i.bi.bi-question-circle.u-spaceRightXs
|#{lineup_entry.member.lastName}
.Panel-cell.u-padXs.u-size2of12
.SelectBox
select.SelectBox-options
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
option(selected=lineup_entry.label==pos) #{pos}
.Panel-cell.u-padXs.u-flexAlignSelfCenter.u-size1of12
.drag-handle
i.bi.bi-grip-vertical.text-secondary
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard.me-1
span Bench
each availability, i in availabilities.filter((a)=>a.eventId==event_id && !context.event_lineup_entries.map((lue)=>lue.memberId).includes(a.memberId) && !a.member.isNonPlayer && a.statusCode!=0 && a.statusCode!==null)
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header.u-padXs.u-size1of12
.u-flexAlignSelfCenter
.Panel-cell.u-padXs.u-size8of12.fw-bold.text-uppercase
if availability.statusCode == 2
i.bi.bi-question-circle-fill.text-info.u-spaceRightXs
else if availability.statusCode == 1
i.bi.bi-check-circle-fill.text-success.u-spaceRightXs
else if availability.statusCode == 0
i.bi.bi-x-circle-fill.text-danger.u-spaceRightXs
else
i.bi.bi-question-circle.u-spaceRightXs
|#{availability.member.lastName}
.Panel-cell.u-padXs.u-size2of12
.SelectBox
select.SelectBox-options
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
option #{pos}
.Panel-cell.u-padXs.u-flexAlignSelfCenter.u-size1of12
.drag-handle
i.bi.bi-grip-vertical.text-secondary
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard-x.me-1
span Out
each availability, i in availabilities.filter((a)=>a.eventId==event_id && !context.event_lineup_entries.map((lue)=>lue.memberId).includes(a.memberId) && !a.member.isNonPlayer && (a.statusCode==0 || a.statusCode===null))
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header.u-padXs.u-size1of12
.u-flexAlignSelfCenter
.Panel-cell.u-padXs.u-size8of12.fw-bold.text-uppercase
if availability.statusCode == 2
i.bi.bi-question-circle-fill.text-info.u-spaceRightXs
else if availability.statusCode == 1
i.bi.bi-check-circle-fill.text-success.u-spaceRightXs
else if availability.statusCode == 0
i.bi.bi-x-circle-fill.text-danger.u-spaceRightXs
else
i.bi.bi-question-circle.u-spaceRightXs
|#{availability.member.lastName}
.Panel-cell.u-padXs.u-size2of12
.SelectBox
select.SelectBox-options
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
option #{pos}
.Panel-cell.u-padXs.u-flexAlignSelfCenter.u-size1of12
.drag-handle
i.bi.bi-grip-vertical.text-secondary

View File

@@ -1,19 +0,0 @@
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - Login
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/font/bootstrap-icons.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body
.u-padSidesMd.u-xs-padSidesLg
.Panel.u-padLg
h3 BenchCoach
p Sign in
a(class="Button Button--large Button--orange" href="/login/federated/teamsnap")
i(class="bi bi-asterisk")/
span Sign in with TeamSnap

View File

@@ -1,17 +0,0 @@
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - #{team.name}
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/font/bootstrap-icons.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body
.container
h2 #{team.name}
p #{team.seasonName}
hr
ul.list-group
a(class="list-group-item" href=`${team.id}/events`) Events
a(class="list-group-item" href=`${team.id}/roster`) Roster

View File

@@ -1,14 +0,0 @@
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - Teams
link(rel='stylesheet' href='/css/bootstrap.min.css')
link(rel='stylesheet' href='/font/bootstrap-icons.min.css')
link(rel='stylesheet' href='/css/teamsnap-ui.css')
body
ul
each team in teams
li
a(class='team' href=`/${team.id}`) #{team.name} [#{team.seasonName}]