reorganize start...

This commit is contained in:
2023-08-19 12:13:41 -05:00
parent c9eaadf688
commit 70a7981ca5
49 changed files with 1189 additions and 7464 deletions

92
src/app.js Normal file
View File

@@ -0,0 +1,92 @@
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");
var bodyParser = require("body-parser");
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(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
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(
"/js",
express.static(path.join(__dirname, "../node_modules/sortablejs"))
);
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;

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

@@ -0,0 +1,42 @@
utils = require("../lib/utils");
exports.getEvents = (req, res, next) => {
team_id = req.params.team_id;
utils.initTeamsnap(req, res, () => {
teamsnap
.bulkLoad(team_id, ["team", "event", "availabilitySummary"])
.then(() => {
items = teamsnap.getAllItems();
context = {
title: "Events",
team: items.find((i) => i.type == "team" && i.id == team_id),
availabilitySummaries: items.filter(
(i) => i.type == "availabilitySummary"
),
events: items.filter((i) => i.type == "event"),
};
res.render("events", context);
});
});
};
exports.getEvent = (req, res, next) => {
team_id = req.params.team_id;
event_id = req.params.event_id;
utils.initTeamsnap(req, res, () => {
teamsnap
.bulkLoad(team_id, ["team", "event", "availabilitySummary"])
.then(() => {
items = teamsnap.getAllItems();
context = {
title: "Event",
team: items.find((i) => i.type == "team" && i.id == team_id),
availabilitySummary: items.find(
(i) => i.type == "availabilitySummary" && i.id == event_id
),
event: items.find((i) => i.type == "event" && i.id == event_id),
};
res.render("event", context);
});
});
};

20
src/controllers/teams.js Normal file
View File

@@ -0,0 +1,20 @@
utils = require("../lib/utils");
exports.getTeams = (req, res, next) => {
utils.initTeamsnap(req, res, () => {
teamsnap.loadTeams().then((teams) => {
context = { title: "Teams", teams: teams };
res.render("teams", context);
});
});
};
exports.getTeamHome = (req, res, next) => {
team_id = req.params.team_id;
utils.initTeamsnap(req, res, () => {
teamsnap.loadTeam(team_id).then((team) => {
context = { title: "Home", team: team };
res.render("team", context);
});
});
};

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

@@ -0,0 +1,35 @@
exports.teamsnapAvailabilitiesSort = (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;
}
}
};
exports.initTeamsnap = (req, res, next) => {
if (!teamsnap.isAuthed()) {
teamsnap.init(process.env["TEAMSNAP_CLIENT_ID"]);
teamsnap.auth(req.user.accessToken);
}
teamsnap.loadCollections((err) => {
teamsnap.enablePersistence();
next(req, res, next);
});
};

661
src/public/css/gamecard.css Normal file
View File

@@ -0,0 +1,661 @@
@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);
}

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 */

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

@@ -0,0 +1,71 @@
@page {
margin: 0;
size: B5;
}
body {
margin: 0;
}
.sheet {
margin: 0;
overflow: hidden;
position: relative;
box-sizing: border-box;
page-break-after: always;
}
/** Paper sizes **/
body.B5 .sheet {
width: 182mm;
height: 257mm;
}
body.index-card .sheet {
width: 3.5in;
height: 5in;
}
/** For screen preview **/
@media screen {
body {
background: #e0e0e0;
}
.sheet {
background: white;
box-shadow: 0 0.5mm 2mm rgba(0, 0, 0, 0.3);
margin: 5mm auto;
}
}
/** Fix for Chrome issue #273306 **/
@media print {
body.A3.landscape {
width: 420mm;
}
body.A3,
body.A4.landscape {
width: 297mm;
}
body.A4,
body.A5.landscape {
width: 210mm;
}
body.A5 {
width: 148mm;
}
body.B5 {
width: 182mm;
}
body.B5.landscape {
width: 257mm;
}
body.letter,
body.legal {
width: 216mm;
}
body.letter.landscape {
width: 280mm;
}
body.legal.landscape {
width: 357mm;
}
}

View File

@@ -0,0 +1,72 @@
/* These styles are generated from project.scss. */
@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");
@import url("../fonts/helvetica-now/stylesheet.css");
.alert-debug {
color: black;
background-color: white;
border-color: #d6e9c6;
}
.alert-error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}
.navbar-brand {
text-transform: uppercase;
font-weight: bolder;
}
.bg-navbar {
background: #212529;
}
.btn-teamsnap {
background-color: #ff8f00;
border-color: #cc7200;
color: #ffffff;
border-bottom-width: 2px;
border-radius: 4px;
cursor: pointer;
display: inline-block;
font-family: "Open Sans", Helvetica, sans-serif;
font-size: 13px;
font-weight: 600;
height: 32px;
line-height: 29px;
margin: 0;
padding: 0 16px;
text-decoration: none;
transition: all 250ms ease-in-out;
vertical-align: middle;
white-space: nowrap;
}
.btn-teamsnap:hover,
.btn-teamsnap:active,
.btn-teamsnap:focus {
background-color: #e68100;
border-color: #b86700;
color: #ffffff;
}
.btn-gamechanger {
color: #fff;
border-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,6 @@
@font-face {
font-family: "Futura Now";
src: url('futura-now.ttf') format('truetype');
font-style: normal;
font-weight: 100 900;
}

View File

@@ -0,0 +1,7 @@
@font-face {
font-family: "Helvetica Now";
src: url('helvetica-now.ttf') format('truetype');
font-weight: 125 900;
font-stretch: 50% 150%;
font-style: normal;
}

View File

@@ -0,0 +1,4 @@
@font-face {
font-family: "Inconsolata";
src: url("inconsolata-vf.ttf") format("truetype");
}

View File

@@ -0,0 +1,38 @@
@font-face {
font-family: 'm+1m';
src: url('mplus-1m-bold-webfont.woff') format('woff');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'm+1m';
src: url('mplus-1m-light-webfont.woff') format('woff');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'm+1m';
src: url('mplus-1m-medium-webfont.woff') format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'm+1m';
src: url('mplus-1m-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'm+1m';
src: url('mplus-1m-thin-webfont.woff') format('woff');
font-weight: 100;
font-style: normal;
}

View File

@@ -0,0 +1,68 @@
@font-face {
font-family: 'Bitstream Vera Sans';
src: url('Vera-Bold-webfont.woff') format('woff');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Bitstream Vera Sans';
src: url('Vera-Bold-Italic-webfont.woff') format('woff');
font-weight: bold;
font-style: oblique;
}
@font-face {
font-family: 'Bitstream Vera Sans';
src: url('Vera-Italic-webfont.woff') format('woff');
font-weight: normal;
font-style: oblique;
}
@font-face {
font-family: 'Bitstream Vera Sans';
src: url('Vera-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Bitstream Vera Sans Mono';
src: url('VeraMono-Bold-webfont.woff') format('woff');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Bitstream Vera Sans Mono';
src: url('VeraMono-Bold-Italic-webfont.woff') format('woff');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Bitstream Vera Sans Mono';
src: url('VeraMono-Italic-webfont.woff') format('woff');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Bitstream Vera Sans Mono';
src: url('VeraMono-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Bitstream Vera Sans Mono';
src: url('VeraMono-webfont.woff') format('woff');
font-weight: 800;
font-style: normal;
}

View File

@@ -0,0 +1,123 @@
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-Black.ttf') format('truetype');
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-BlackItalic.ttf') format('truetype');
font-weight: 800;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-BoldItalic.ttf') format('truetype');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondBlack.ttf') format('truetype');
font-stretch: condensed;
font-weight: 800;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondBlackItalic.ttf') format('truetype');
font-stretch: condensed;
font-weight: 800;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondBold.ttf') format('truetype');
font-stretch: condensed;
font-weight: bold;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondBoldItalic.ttf') format('truetype');
font-stretch: condensed;
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondItalic.ttf') format('truetype');
font-stretch: condensed;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondLight.ttf') format('truetype');
font-stretch: condensed;
font-weight: 300;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondLightItalic.ttf') format('truetype');
font-stretch: condensed;
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondRegular.ttf') format('truetype');
font-stretch: condensed;
font-style: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondSemiBold.ttf') format('truetype');
font-stretch: condensed;
font-weight: 600;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-CondSemiBoldItalic.ttf') format('truetype');
font-stretch: condensed;
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-Italic.ttf') format('truetype');
font-style: italic;
font-weight: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-Light.ttf') format('truetype');
font-weight: 300;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-LightItalic.ttf') format('truetype');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'VerdanaPro';
src: url('VerdanaPro-SemiBoldItalic.ttf') format('truetype');
font-weight: 600;
font-style: italic;
}

View File

@@ -0,0 +1,10 @@
<?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>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

123
src/routes/auth.js Normal file
View File

@@ -0,0 +1,123 @@
var express = require("express");
var passport = require("passport");
var TeamsnapStrategy = require("passport-teamsnap");
// Configure the TeamSnap strategy for use by Passport.
//
// OAuth 2.0-based strategies require a `verify` function which receives the
// credential (`accessToken`) for accessing the Facebook API on the user's
// 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
// authentication.
passport.use(
new TeamsnapStrategy(
{
apiVersion: "3",
clientID: process.env["TEAMSNAP_CLIENT_ID"],
clientSecret: process.env["TEAMSNAP_CLIENT_SECRET"],
callbackURL: "/auth/teamsnap/callback",
passReqToCallback: true,
scope: ["read", "write"],
},
function (req, accessToken, refreshToken, profile, done) {
json = JSON.parse(profile._raw);
new_profile = { access_token: accessToken };
new_profile["id"] = json.collection.items[0].data.filter(
(e) => e.name == "id"
)[0].value;
new_profile["email"] = json.collection.items[0].data.filter(
(e) => e.name == "email"
)[0].value;
new_profile["first_name"] = json.collection.items[0].data.filter(
(e) => e.name == "first_name"
)[0].value;
req.session.teamsnap_access_token = accessToken;
teamsnap.init(process.env["TEAMSNAP_CLIENT_ID"]);
teamsnap.auth(accessToken);
// teamsnap.enablePersistence();
return done(null, new_profile);
}
)
);
// Configure Passport authenticated session persistence.
//
// In order to restore authentication state across HTTP requests, Passport needs
// to serialize users into and deserialize users out of the session. In a
// production-quality application, this would typically be as simple as
// supplying the user ID when serializing, and querying the user record by ID
// from the database when deserializing. However, due to the fact that this
// example does not have a database, the complete Facebook profile is serialized
// and deserialized.
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
console.log("L#51 serializing user id", user.id);
cb(null, {
id: user.id,
username: user.email,
name: user.firstName,
accessToken: user.access_token,
});
});
});
passport.deserializeUser(function (user, cb) {
process.nextTick(function () {
return cb(null, user);
});
});
var router = express.Router();
/* GET /login
*
* This route prompts the user to log in.
*
* The 'login' view renders an HTML page, which contain a button prompting the
* user to sign in with TeamSnap. When the user clicks this button, a request
* will be sent to the `GET /login/federated/teamsnap` route.
*/
router.get("/login", function (req, res, next) {
// https://stackoverflow.com/a/73056806/20522015
returnTo = req.session.returnTo;
// req.session.regenerate(); // this is not working right as of now...
req.session.returnTo = returnTo;
res.render("login");
});
/* GET /login/federated/teamsnap
*
* This route redirects the user to TeamSnap, where they will authenticate.
*
* Signing in with TeamSnap is implemented using OAuth 2.0. This route initiates
* an OAuth 2.0 flow by redirecting the user to TeamSnap's identity server.
* Once there, TeamSnap will authenticate the user
* and obtain their consent to release identity information to this app.
*
* Once TeamSnap has completed their interaction with the user, the user will be
* redirected back to the app.
*/
router.get("/login/federated/teamsnap", passport.authenticate("teamsnap"));
/*
This route completes the authentication sequence when TeamSnap redirects the
user back to the application. When a new user signs in, a user account is
automatically created and their TeamSnap account is linked. When an existing
user returns, they are signed in to their linked account.
*/
router.get(
"/auth/teamsnap",
passport.authenticate("teamsnap", function (err, user, info, status) {})
);
router.get(
"/auth/teamsnap/callback",
passport.authenticate("teamsnap", {
successReturnToOrRedirect: "/",
failureRedirect: "/login",
keepSessionInfo: true,
})
);
module.exports = router;

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

@@ -0,0 +1,317 @@
var express = require("express");
var ensureLogIn = require("connect-ensure-login").ensureLoggedIn;
var papaparse = require("papaparse");
var ensureLoggedIn = ensureLogIn();
var router = express.Router();
var multer = require("multer");
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
const teamsController = require("../controllers/teams");
const eventsController = require("../controllers/events");
router.get("/", ensureLoggedIn, teamsController.getTeams);
router.get("/:team_id/home", ensureLoggedIn, teamsController.getTeamHome);
router.get("/:team_id/events", ensureLoggedIn, eventsController.getEvents);
router.get(
"/:team_id/event/:event_id",
ensureLoggedIn,
eventsController.getEvent
);
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 = {
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(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 = {
title: "Lineup",
team: items.find((e) => e.type == "team" && e.id == team_id),
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/lineup", context);
});
});
}
);
router.get("/:team_id/events", ensureLoggedIn);
router.get("/:team_id/opponents", ensureLoggedIn, function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
teamsnap.loadCollections(function (err) {
teamsnap.bulkLoad(team_id, ["team", "opponent"]).then((items) => {
res.set("Content-Type", "text/html");
res.render("opponents", {
title: "Opponents",
team: items.find((i) => i.type == "team" && i.id == team_id),
opponents: items.filter((i) => i.type == "opponent"),
team_id: team_id,
});
});
});
});
router.get("/:team_id/opponents", ensureLoggedIn, function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
teamsnap.loadCollections(function (err) {
teamsnap.bulkLoad(team_id, ["team", "opponent"]).then((items) => {
res.set("Content-Type", "text/html");
res.render("opponents", {
title: "Opponents",
team: items.find((i) => i.type == "team" && i.id == team_id),
opponents: items.filter((i) => i.type == "opponent"),
team_id: team_id,
});
});
});
});
router.get("/:team_id/roster", ensureLoggedIn, function (req, res, next) {
authTeamsnap(req.user);
team_id = req.params.team_id;
teamsnap.loadCollections(function (err) {
teamsnap.enablePersistence();
teamsnap.bulkLoad(team_id, ["team", "members"]).then(() => {
items = teamsnap.getAllItems();
res.set("Content-Type", "text/html");
res.render("roster", {
title: `Roster`,
team: items.find((i) => i.type == "team" && i.id == team_id),
members: items.find((i) => i.type == "member" && team.id == team_id),
team_id: team_id,
});
});
});
});
router.get(
"/:team_id/opponent/:opponent_id/upload-logo",
ensureLoggedIn,
function (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,
});
}
);
router.post(
"/:team_id/opponent/:opponent_id/upload-logo",
ensureLoggedIn,
upload.single("file"),
function (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));
}
);
module.exports = router;

42
src/views/base.pug Normal file
View File

@@ -0,0 +1,42 @@
html
head
block meta
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title BenchCoach - #{title}
block styles
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')
link(rel='stylesheet' href='/css/project.css')
block pre-scripts
script(type='text/javascript', src='/js/Sortable.js')
body.bg-light
block navbar
.benchcoach-nav.u-flex.u-flexJustifyBetween.u-flexAlignItemsCenter
.u-flex.u-flexAlignItemsCenter
img(src='/media/benchcoach.svg' alt='TeamSnap Logo' width='30' height='30')
h3.u-padLeft.u-colorWhite.u-noMarginBottom BenchCoach
.u-spaceSm.u-md-spaceLg
block content
.Panel
.Panel-header
h2.Panel-title Panel
.Panel-body
.Panel-row Panel Row
.Panel-row Roster
.Panel-row Opponents
block footer
.u-padMd.u-borderTop
.u-max1200.u-flexExpandSides.Grid
.Grid-cell.u-md-size1of2
p.u-textCenter.u-md-textLeft
| Copyright &copy; 2023 BenchCoach. BenchCoach is not affiliated with TeamSnap, inc.
.Grid-cell.u-md-size1of2
p.u-textCenter.u-md-textRight
block scripts

8
src/views/error.pug Normal file
View File

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

View File

@@ -0,0 +1,409 @@
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

@@ -0,0 +1,63 @@
extends ../base.pug
include lineup-slot.pug
include ../widgets/availability-progress-bar.pug
block append styles
link(rel='stylesheet' href='/css/lineup.css')
block pre-scripts
script(type='text/javascript', src='/js/Sortable.js')
block content
div(id=`event-lineup-${event.id}`).event-lineup
.Panel
.panel-header
.Panel-title #{event.formattedTitle}
.Panel-body
.Panel-row
p.text-muted.mb-2 #{event.startDate}
p #{event.locationName}
+availability-progress-bar(availabilitySummary, team)
.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"]
.Grid-cell.position-status #{pos}
.slot-set
each lineup_entry, i in event_lineup_entries_offense
+lineup-slot(lineup_entry, i)
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard-minus.me-1
span Starting Lineup (Position Only)
.slot-set
each lineup_entry, i in event_lineup_entries
if lineup_entry.label.includes("[PO]")
+lineup-slot(lineup_entry, i)
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard.me-1
span Bench
.slot-set
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)
+lineup-slot(availability, i)
.Panel
.Panel-body
.Panel-row.Panel-title.u-padXs
i.bi.bi-clipboard-x.me-1
span Out
.slot-set
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))
+lineup-slot(availability, i)
block scripts
script
include lineup.js
script.
colorPositions();

27
src/views/event.pug Normal file
View File

@@ -0,0 +1,27 @@
extends base.pug
include mixin-availability-progress-bar.pug
block content
.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
+availability-progress-bar(availabilitySummary, team)
.Panel-row
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

21
src/views/events.pug Normal file
View File

@@ -0,0 +1,21 @@
extends base.pug
include mixin-availability-progress-bar.pug
block content
.Panel
.Panel-header
.Panel-title Schedule
.Panel-body
each event in events
- var availabilitySummary = availabilitySummaries.find((a)=>a.eventId==event.id)
a(class="event list-group-item" href=`/${team_id}/event/${event.id}`).Panel-row--withCells
.Panel-cell
h4 #{event.formattedTitle}
+availability-progress-bar(availabilitySummary, team)
.Panel-cell
|#{event.startDate.toLocaleDateString("en-us",{weekday: "short", day: "numeric",month: "short"})}
.Panel-cell
|#{event.startDate.toLocaleTimeString("en-us",{hour: "numeric", minute: "2-digit"})}
.Panel-cell
|#{event.locationName}

24
src/views/home.pug Normal file
View File

@@ -0,0 +1,24 @@
extends base.pug
block content
.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}/home`) #{team.name} [#{team.seasonName}]
else
a.btn.btn-outline-primary(href="login")
| Login

356
src/views/lineup.js Normal file
View File

@@ -0,0 +1,356 @@
/* Project specific Javascript goes here. */
function onPositionSelectChange(elem) {
elem.querySelectorAll("option").forEach((option) => {
if (option.innerText == elem.value) {
option.setAttribute("selected", "selected");
} else {
option.removeAttribute("selected");
}
});
colorPositions();
}
function colorPositions() {
console.log("Coloring Positions");
for (bcLineup of document.getElementsByClassName("event-lineup")) {
selected_lineup_positions = Array.from(
bcLineup.querySelectorAll(
".Panel-row .SelectBox.position-selection option[selected='selected']"
)
).map((el) => el.value);
console.log(selected_lineup_positions);
for (position_status of bcLineup.querySelectorAll(".position-status")) {
for (class_name of ["text-danger", "text-warning", "text-success"]) {
if (position_status.classList.contains(class_name)) {
position_status.classList.remove(class_name);
}
}
occurrences = selected_lineup_positions.filter(
(s) => s == position_status.innerText
).length;
if (occurrences == 1) {
position_status.classList.add("text-success");
} else if (occurrences > 1) {
position_status.classList.add("text-warning");
} else {
position_status.classList.add("text-danger");
}
}
}
}
function refresh_lineup_order(itemEl) {
let bcLineup = itemEl.closest(".benchcoach-lineup");
var player_rows = [];
for (tbody of bcLineup.querySelectorAll(
"[class*='tbody-benchcoach-starting']"
)) {
for (row of tbody.rows) {
player_rows.push(row);
}
}
for (let i = 0; i < player_rows.length; i++) {
var player_order = player_rows[i].querySelector('[id^="sequence"]');
var form_element_order = player_rows[i].querySelector('[id$="sequence"]');
player_order.innerText = parseInt(player_rows[i].dataset.order);
player_rows[i].dataset.order = i;
form_element_order.value = i;
player_order.innerHTML = i + 1;
}
var player_rows = bcLineup.getElementsByClassName("tbody-benchcoach-bench")[0]
.rows;
for (let i = 0; i < player_rows.length; i++) {
var player_order = player_rows[i].querySelector('[id^="sequence"]');
var form_element_order = player_rows[i].querySelector('[id$="sequence"]');
player_rows[i].dataset.order = null;
form_element_order.value = null;
player_order.innerHTML = null;
}
}
function sendToClipboard(itemEl) {
let bcLineup = itemEl.closest(".benchcoach-lineup");
player_rows = bcLineup.querySelectorAll("[data-position=P]");
lineup_export = [];
if (player_rows.length > 0) {
lineup_export.push(player_rows[0].dataset.playerName);
lineup_export.push("", "");
} else {
lineup_export.push("", "", "");
}
lineup_export.push("");
for (position of ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"]) {
var player_rows = bcLineup.querySelectorAll(
`[data-position=${CSS.escape(position)}]`
);
if (player_rows.length > 0) {
lineup_export.push(player_rows[0].dataset.playerName);
} else {
lineup_export.push("");
}
}
for (position of ["EH"]) {
var player_rows = bcLineup.querySelectorAll(
`[data-position=${CSS.escape(position)}]`
);
for (var i = 0; i < 2; i++) {
if (i < player_rows.length) {
lineup_export.push(player_rows[i].dataset.playerName);
} else {
lineup_export.push("");
}
}
}
for (position of ["DR"]) {
let player_rows = bcLineup.querySelectorAll(
`[data-position=${CSS.escape(position)}]`
);
if (player_rows.length > 0) {
lineup_export.push(player_rows[0].dataset.playerName);
} else {
lineup_export.push("");
}
}
lineup_export.push("");
lineup_export.push("", "");
lineup_export.push("");
for (var i = 0; i < 11; i++) {
let player_rows = bcLineup
.querySelector(".table-benchcoach-startinglineup")
.querySelectorAll(`[data-order=${CSS.escape(i)}]`);
if (player_rows.length > 0) {
lineup_export.push(player_rows[0].dataset.playerName);
} else {
lineup_export.push("");
}
}
console.dir(lineup_export);
var textArea = document.createElement("textarea");
textArea.value = lineup_export.join("\n");
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand("copy");
var msg = successful ? "successful" : "unsuccessful";
console.log("Copying text command was " + msg);
} catch (err) {
console.error("Oops, unable to copy", err);
}
document.body.removeChild(textArea);
}
for (bcLineup of document.querySelectorAll(".Panel:has(.lineup-slot)")) {
var startinglineup = new Sortable.create(
bcLineup.querySelector(".slot-set"),
{
animation: 150,
handle: ".drag-handle",
ghostClass: "ghost",
group: {
name: bcLineup.id,
put: [bcLineup.id],
pull: [bcLineup.id],
},
onAdd: function (/**Event*/ evt) {
// Add to Lineup
var itemEl = evt.item; // dragged HTMLElement
var player_order = itemEl.querySelector('[id^="sequence-member"]');
var player_available = itemEl.querySelector(
'[class^="member-availability-status"]'
);
refresh_lineup_order(itemEl);
if (player_order.classList.contains("d-none")) {
player_order.classList.remove("d-none");
}
// player_available.classList.add('d-none')
},
onUpdate: function (/**Event*/ evt) {
console.log("update to lineup");
var itemEl = evt.item; // dragged HTMLElement
refresh_lineup_order(itemEl);
},
}
);
}
function copyEmailTable(itemEl, subject, recipients) {
// Create container for the HTML
// [1]
let bcLineup = itemEl.closest(".benchcoach-lineup");
var container = document.createElement("div");
var tbl = document.createElement("table");
let thead = tbl.createTHead();
let thead_row = thead.insertRow();
let thead_row_cell = thead_row.insertCell();
thead_row_cell.appendChild(
document
.createElement("h3")
.appendChild(document.createTextNode("STARTING LINEUP"))
);
thead_row_cell.colSpan = 3;
thead_row_cell.classList.add("title-cell");
var tbody = tbl.createTBody();
for (row of bcLineup.querySelector(".table-benchcoach-startinglineup").rows) {
let tr = tbody.insertRow();
cell = tr.insertCell();
cell.classList.add("sequence-cell");
cell.appendChild(document.createTextNode(parseInt(row.dataset.order) + 1));
cell = tr.insertCell();
cell.appendChild(document.createTextNode(row.dataset.playerName));
cell.classList.add("name-cell");
tr.insertCell().appendChild(document.createTextNode(row.dataset.position));
}
if (
bcLineup.querySelector(".table-benchcoach-startingpositionalonly").rows
.length > 0
) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.colSpan = 3;
cell.appendChild(document.createTextNode("STARTING (POS. ONLY)"));
cell.classList.add("title-cell");
for (row of bcLineup.querySelector(
".table-benchcoach-startingpositionalonly"
).rows) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.classList.add("sequence-cell");
cell.appendChild(document.createTextNode(""));
cell = tr.insertCell();
cell.appendChild(document.createTextNode(row.dataset.playerName));
cell.classList.add("name-cell");
tr.insertCell().appendChild(
document.createTextNode(row.dataset.position)
);
}
}
if (bcLineup.querySelector(".table-benchcoach-bench").rows.length > 0) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.colSpan = 3;
cell.appendChild(document.createTextNode("SUBS"));
cell.classList.add("title-cell");
for (row of bcLineup.querySelector(".table-benchcoach-bench").rows) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.classList.add("sequence-cell");
availability_status = {
None: "UNK",
0: "NO",
2: "MAY",
1: "YES",
}[row.dataset.availabilityStatuscode];
cell.appendChild(document.createTextNode(availability_status));
cell = tr.insertCell();
cell.appendChild(document.createTextNode(row.dataset.playerName));
cell.classList.add("name-cell");
tr.insertCell().appendChild(document.createTextNode(""));
}
}
if (bcLineup.querySelector(".table-benchcoach-out").rows.length > 0) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.colSpan = 3;
cell.appendChild(document.createTextNode("OUT"));
cell.classList.add("title-cell");
for (row of bcLineup.querySelector(".table-benchcoach-out").rows) {
var tr = tbody.insertRow();
cell = tr.insertCell();
cell.classList.add("sequence-cell");
availability_status = {
None: "UNK",
0: "NO",
1: "MAY",
2: "YES",
}[row.dataset.availabilityStatuscode];
cell.appendChild(document.createTextNode(availability_status));
tr.insertCell().appendChild(
document.createTextNode(row.dataset.playerName)
);
tr.insertCell().appendChild(document.createTextNode(""));
}
}
container.appendChild(tbl);
for (cell of container.getElementsByClassName("title-cell")) {
cell.setAttribute(
"style",
"font-weight:bold;background-color:#323669;color:#fff;padding:2px 5px;"
);
}
for (cell of container.getElementsByClassName("sequence-cell")) {
cell.setAttribute("style", "font-weight:bold;padding:2px 5px;");
}
for (cell of container.getElementsByClassName("name-cell")) {
cell.setAttribute("style", "width:200px;");
}
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice
.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled;
});
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container);
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges();
var range = document.createRange();
range.selectNode(container);
window.getSelection().addRange(range);
// [5.1]
document.execCommand("copy");
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true;
// [5.3]
// document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++)
activeSheets[i].disabled = false;
// Remove the container
// [6]
document.body.removeChild(container);
subject_encoded = encodeURIComponent(subject);
window.open(
"readdle-spark://compose?recipient=manager@chihounds.com&subject=" +
subject +
"&bcc=" +
recipients
);
}

19
src/views/login.pug Normal file
View File

@@ -0,0 +1,19 @@
extends base.pug
block content
.Grid.Grid--fit.Grid--withGutter.u-max1200.u-flexExpandSides.u-xs-size5of6.u-sm-size2of3.u-md-sizeFull.u-padBottomMd.u-xs-padEndsLg.u-sm-padEndsXl
.Grid-cell.u-size5of12
.Panel.u-padLg.u-spaceSidesAuto
h1.u-spaceSidesAuto.u-spaceBottomLg Sign in
a(class="Button Button--large Button--orange" href="/login/federated/teamsnap").u-spaceSidesAuto
img(src="/media/teamsnap_star.svg").icon.u-spaceRightSm
span TeamSnap
.Grid-cell.u-size7of12.u-textCenter
h1
img(src="media/benchcoach.svg" style="width: 2.5em;")
h1
strong
| Welcome to
span.text-nowrap BenchCoach
.lead.fst-italic.fw-light
| An assistant coach for TeamSnap

View File

@@ -0,0 +1,15 @@
mixin availability-progress-bar(availabilitySummary, team)
.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

@@ -0,0 +1,22 @@
mixin lineup-slot(item, index)
-
if (item.type == "eventLineupEntry") {
var availability_status_code = item.availabilityStatusCode
} else if (item.type == "availability") {
var availability_status_code = item.statusCode
}
.Panel-row.Panel-row--withCells.lineup-slot
.Panel-cell.Panel-cell--header.u-padXs.u-size1of12
.u-flexAlignSelfCenter
|#{index+1}
div(class=`Panel-cell availability-status-code-${availability_status_code}`).u-padXs.u-size8of12
span.lastname #{item.member.lastName}
span.jerseynumber ##{item.member.jerseyNumber}
.Panel-cell.u-padXs.u-size2of12
.SelectBox.position-selection
select(onchange="onPositionSelectChange(this)").position-select-box.SelectBox-options
each pos in ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH"]
option(selected=item.label==pos) #{pos}
.Panel-cell.u-padXs.u-flexAlignSelfCenter.u-size1of12
.drag-handle
i.bi.bi-grip-vertical.text-secondary.drag-handle

48
src/views/opponent.pug Normal file
View File

@@ -0,0 +1,48 @@
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
.container
.Panel
.Panel-header
.Panel-title #{opponent.name}
.Panel-body
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header ID
.panel-cell #{opponent.id}
Button(onclick=`navigator.clipboard.writeText("${opponent.id}");`).Button
i.bi.bi-clipboard.Icon
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header Contact Name
.panel-cell #{opponent.contactsName}
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header Contact Phone
.panel-cell #{opponent.contactsPhone}
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header Contact Email
.panel-cell #{opponent.contactsEmail}
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header Logo
.panel-cell
if opponent_logo
img(src=`${opponent_logo.mediumUrl}` width="64" height="64")
a.Button(target="_blank" rel="noopener noreferrer" href=`https://go.teamsnap.com/${team_id}/files/view/${opponent_logo.id}`).Button
i.bi.bi-asterisk.Icon
else
if team_media_group
a.Button(target="_blank" rel="noopener noreferrer" href=`https://go.teamsnap.com/${team_id}/files/list/${team_media_group.id}`) Upload
else
a.Button(target="_blank" rel="noopener noreferrer" href=`https://go.teamsnap.com/${team_id}/files/`) Upload
Button(onclick=`navigator.clipboard.writeText("team-logo-${opponent.id}.png");`).Button
i.bi.bi-clipboard.Icon
span Copy Filename
.Panel-row.Panel-row--withCells
.Panel-cell.Panel-cell--header Notes
.panel-cell #{opponent.Notes}

18
src/views/opponents.pug Normal file
View File

@@ -0,0 +1,18 @@
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
.container
.Panel
.Panel-header
.Panel-title Opponents
.Panel-body
each opponent in opponents
.Panel-row
a(class='opponent' href=`/${team.id}/opponent/${opponent.id}`) #{opponent.name}

17
src/views/roster.pug Normal file
View File

@@ -0,0 +1,17 @@
extends base.pug
include widgets/availability-progress-bar.pug
block content
.Panel
.Panel-header
.Panel-title roster
.Panel-body
each member in members
- var availabilitySummary = availabilitySummaries.find((a)=>a.eventId==event.id)
a(class="event list-group-item" href=`/${team_id}/event/${event.id}`).Panel-row--withCells
.Panel-cell
|#{member.firstName}
.Panel-cell
|#{event.lastName}

24
src/views/team.pug Normal file
View File

@@ -0,0 +1,24 @@
extends base.pug
block content
.Panel
.Panel-body
.Panel-row
h2.Panel-title #{team.name}
p #{team.seasonName}
.Panel
.Panel-body
.Panel-row
a(href=`/${team.id}/events`).u-fontSizeXl-
<svg viewBox="0 0 960 960" role="presentation" class="Icon">
<path d="M890 131c11.3 2.7 20.5 8.3 27.5 17s10.5 18.7 10.5 30v699c0 11.3-3.5 21.3-10.5 30s-16.2 14.3-27.5 17l-8 2c-4.7.7-11 1.7-19 3s-17.7 3-29 5-24.3 3.8-39 5.5-30.5 3.5-47.5 5.5-35.7 4-56 6-41.3 3.7-63 5-45.3 2.3-71 3-51.5 1-77.5 1-51.8-.3-77.5-1-49.3-1.7-71-3-42.7-3-63-5-39-4-56-6-32.8-3.8-47.5-5.5-27.7-3.5-39-5.5-21-3.7-29-5-14.3-2.3-19-3l-8-2c-11.3-2.7-20.5-8.3-27.5-17S32 888.3 32 877V178c0-11.3 3.5-21.3 10.5-30s16.2-14.3 27.5-17c.7 0 4.5-.7 11.5-2s17.8-3.2 32.5-5.5 30-4.5 46-6.5v43c0 23.3 8.3 42.5 25 57.5s36.7 22.5 60 22.5c-14-12.7-21-28.7-21-48V64c0-17.3 6.3-32.3 19-45s27.7-19 45-19 32.3 6.3 45 19 19 27.7 19 45v35c40-2 82.7-3 128-3 28.7 0 50 .3 64 1v63c0 23.3 8.3 42.5 25 57.5s36.7 22.5 60 22.5c-14-12.7-21-28.7-21-48V64c0-17.3 6.3-32.3 19-45s27.7-19 45-19 32.3 6.3 45 19 19 27.7 19 45v45c64 6.7 115.3 14 154 22zM480 869c122.7 0 240-9.3 352-28V384H128v457c112 18.7 229.3 28 352 28zm256-421v128H608V448h128zm-192 0v128H416V448h128zm0 320H416V640h128v128zm-192 0H224V640h128v128zm384 0H608V640h128v128z"></path>
</svg>
| Events
.Panel-row
a(href=`/${team.id}/roster`)
<svg viewBox="0 0 960 960" class="Icon"><path d="M959 714c2 8 .7 16-4 24-5.3 7.3-12.3 11.7-21 13-9.3 4.7-66.7 8.3-172 11-28.7-60.7-73.3-109.7-134-147-17.3-10-39-19.7-65-29-6.7-3.3-12-5.7-16-7 31.3-49.3 52.3-97 63-143s13.5-84.3 8.5-115-15.2-58.3-30.5-83c-28-47.3-63.3-80.7-106-100 18.7-24 37.2-41.7 55.5-53S579 68 607 68c33.3 0 63.7 7.7 91 23s49.3 36.7 66 64c43.3 71.3 32.3 157.7-33 259-20 28.7-27.7 50.7-23 66 2 7.3 8 14.5 18 21.5s18.7 12.2 26 15.5 19.3 8.3 36 15c26 10.7 43.7 18.7 53 24 64.7 39.3 104 92 118 158zM352 892c-70 0-132-1-186-3s-90.2-4-108.5-6-28.8-3.3-31.5-4c-8.7-.7-15.7-5-21-13-4.7-6.7-6-14.7-4-24 14-66 53-118.7 117-158 8-4.7 25.7-12.7 53-24 16.7-6.7 28.7-11.7 36-15s16-8.5 26-15.5 16-14.2 18-21.5c4.7-14.7-2.7-36.7-22-66-65.3-102-76.7-188.3-34-259 16.7-27.3 38.8-48.7 66.5-64s57.8-23 90.5-23c33.3 0 63.8 7.7 91.5 23s49.8 36.7 66.5 64c42.7 70.7 31.3 157-34 259-19.3 29.3-26.7 51.3-22 66 2 7.3 8 14.5 18 21.5s18.7 12.2 26 15.5 19.3 8.3 36 15c27.3 11.3 45 19.3 53 24 63.3 39.3 102.7 92 118 158 2 8.7.3 16.7-5 24-5.3 8-12.3 12.3-21 13-2.7.7-13.3 2-32 4s-55 4-109 6-116 3-186 3z"></path></svg>
| Roster
.Panel-row
a(href=`/${team.id}/opponents`)
<svg viewBox="0 0 960 960" class="Icon"><path d="M959 628c2 6.7.7 13.7-4 21s-10.7 11.3-18 12l-7 1c-4.7.7-11.8 1.5-21.5 2.5s-21.5 2-35.5 3-33.2 1.8-57.5 2.5-50.8 1.3-79.5 2c-6-4.7-12.7-9.2-20-13.5s-15.2-9-23.5-14S678 635.7 674 633c-10.7-5.3-21-11.3-31-18 70-92.7 84-184.7 42-276-16.7-36-40-64-70-84s-62.7-32.3-98-37c0-23.3 5.3-47.3 16-72 12.7-28 32-48.7 58-62s53-20 81-20 55 6.7 81 20 45.3 34 58 62c32 70 16.3 141.7-47 215-19.3 22.7-18 46.3 4 71 6.7 8 26.2 21.3 58.5 40s55.2 32.7 68.5 42c7.3 5.3 13.8 10.2 19.5 14.5s10.5 9.3 14.5 15 7.2 10 9.5 13 4.7 8.2 7 15.5 3.8 12.3 4.5 15 2.2 9.2 4.5 19.5 3.8 17.5 4.5 21.5zm-673 5c-4 2.7-10.2 6.5-18.5 11.5s-16.2 9.7-23.5 14-14 8.8-20 13.5c-20.7 0-40.3-.3-59-1s-34.7-1.3-48-2-25.3-1.3-36-2-19.5-1.3-26.5-2-12.8-1.3-17.5-2-8.3-1-11-1l-3-1c-7.3-.7-13.3-4.7-18-12-4.7-6-6-13-4-21 1.3-4.7 3-11.8 5-21.5s3.3-16.2 4-19.5 2.2-8.5 4.5-15.5 4.7-12 7-15S27 549.3 31 544s8.7-10.3 14-15 12-9.7 20-15c14-10 37-24.3 69-43s51.3-31.7 58-39c22-24.7 23.3-48.3 4-71-63.3-73.3-79-145-47-215 12.7-28 32-48.7 58-62s53-20 81-20 55 6.7 81 20 45.3 34 58 62c10.7 24.7 16 48.7 16 72-80 11.3-136.3 51.7-169 121-41.3 91.3-27 183.3 43 276-2.7 2-5.7 4-9 6s-7.2 4.2-11.5 6.5-7.5 4.2-9.5 5.5h-1zm194 263c-28 0-54.5-.2-79.5-.5s-46.2-.8-63.5-1.5-33-1.5-47-2.5-25.5-1.8-34.5-2.5-16.5-1.3-22.5-2-10.3-1-13-1l-5-1c-7.3-.7-13.3-4.7-18-12-4.7-6-6-13-4-21 1.3-4.7 3-11.8 5-21.5s3.3-16.2 4-19.5 2.2-8.5 4.5-15.5 4.7-12 7-15 5.5-7.2 9.5-12.5 8.7-10.3 14-15 12-9.7 20-15c14-10 37-24.3 69-43s51.3-31.7 58-39c22-24.7 23.3-48.3 4-71-63.3-73.3-79-145-47-215 12.7-28 32-48.7 58-62s53-20 81-20 55 6.7 81 20 45.3 34 58 62c32 70 16.3 141.7-47 215-19.3 22.7-18 46.3 4 71 6.7 8 26.2 21.3 58.5 40s55.2 32.7 68.5 42c16.7 12 29 23.2 37 33.5s13.5 21.7 16.5 34S763 833.3 767 852c2 7.3.7 14.3-4 21-4.7 7.3-10.7 11.3-18 12l-5 1c-2.7 0-7 .3-13 1s-13.5 1.3-22.5 2-20.7 1.5-35 2.5-30.2 1.8-47.5 2.5-38.3 1.2-63 1.5-51 .5-79 .5z"></path></svg>
| Opponents

20
src/views/teams.pug Normal file
View File

@@ -0,0 +1,20 @@
extends base.pug
block content
.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
ul.list-group
each team in teams
a(class='team list-group-item' href=`/${team.id}/home`) #{team.name} [#{team.seasonName}]

84
src/views/upload-logo.pug Normal file
View File

@@ -0,0 +1,84 @@
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')
script(type='text/javascript', src='/js/teamsnap.js')
body
.container
.Panel
.Panel-header
.Panel-title Upload
.Panel-body
.Panel-row
.input-group
label(for='file') Select file
input(id='file' type="file")
input(id="csrf_token", type="hidden" name="csrf_token" value=`${csrf_token}`)
input(id='team_id', type="hidden", value=`${team_id}`)
input(id='opponent_id', type="hidden", value=`${opponent_id}`)
input(id='token', type="hidden", value=`${token}`)
input(id='clientId', type="hidden", value=`${clientId}`)
button(type='submit' onClick="submitForm()") Upload
script.
function submitForm(e) {
console.log(document.getElementById("opponent_id").value)
const file = document.getElementById("file");
const csrf_token = document.getElementById("csrf_token").value;
const team_id = document.getElementById("team_id").value;
const opponent_id = document.getElementById("opponent_id").value;
const formData = new FormData();
formData.append("file", file.files[0]);
formData.append("team_id", team_id);
formData.append("opponent_id", opponent_id);
for (var key of formData.entries()) {
console.log(key[0] + ', ' + key[1]);
}
fetch(`/${team_id}/opponent/${opponent_id}/upload-logo`, {
method: 'POST',
body: formData,
headers: {
"X-CSRF-Token": csrf_token,
}
})
.then((res) => console.log(res))
.catch((err) => ("Error occured", err));
}
function uploadImageForm(e) {
const file = document.getElementById("file");
const csrf_token = document.getElementById("csrf_token").value;
const team_id = document.getElementById("team_id").value;
const opponent_id = document.getElementById("opponent_id").value;
const token = document.getElementById("token").value;
const clientId = document.getElementById("clientId").value;
const formData = new FormData();
teamsnap.init(clientId);
teamsnap.auth(token)
console.log(teamsnap.isAuthed())
teamsnap
.loadCollections()
.then(() => {
return teamsnap.createTeamMedium({
file: file.files[0],
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));
}