Merge branch 'testing' into 'main'

v1.2.0

See merge request sudoer777/score-tracker!16
main
Ethan Reece 2022-03-10 01:19:49 +00:00
commit bb9c583ed2
61 changed files with 6182 additions and 753 deletions

16
.env.example 100644
View File

@ -0,0 +1,16 @@
NODE_ENV=development
PGUSER=dbuser
PGHOST=database.server.com
PGPASSWORD=dbuserpassword
PGDATABASE=mydatabase
PGPORT=5432
PUBLIC_SUBMIT_PAGE=false
#MAIL_FROM=fromaddress@example.com
#MAIL_HOST=smtp.smtphost.net
#MAIL_PORT=465
#MAIL_SECURE=true
#MAIL_USER=username
#MAIL_PASS=password

View File

@ -1 +1 @@
8.11.1 16.13.0

17
.vscode/launch.json vendored 100644
View File

@ -0,0 +1,17 @@
{
// 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": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www"
}
]
}

View File

@ -1,4 +1,4 @@
FROM node:8.11-alpine FROM node:16.13.0-alpine3.12
WORKDIR /usr/src/app WORKDIR /usr/src/app

View File

@ -1,17 +1,53 @@
### Node Express template project # Score Tracker
This project is based on a GitLab [Project Template](https://docs.gitlab.com/ee/gitlab-basics/create-project.html). Main repository: https://gitlab.sudoer.ch/sudoer777/score-tracker/
Improvements can be proposed in the [original project](https://gitlab.com/gitlab-org/project-templates/express). A web app designed to collect and display scores for sports
### CI/CD with Auto DevOps ## Branches
This template is compatible with [Auto DevOps](https://docs.gitlab.com/ee/topics/autodevops/). - [main](https://gitlab.sudoer.ch/sudoer777/score-tracker/-/tree/main) - Stable, production-ready code
- [testing](https://gitlab.sudoer.ch/sudoer777/score-tracker/-/tree/testing) - Nearly complete code being tested
- [develop](https://gitlab.sudoer.ch/sudoer777/score-tracker/-/tree/develop) - Unstable code under development
If Auto DevOps is not already enabled for this project, you can [turn it on](https://docs.gitlab.com/ee/topics/autodevops/#enabling-auto-devops) in the project settings. ## Installation
### Developing with Gitpod This repository can be cloned and then pushed to Heroku/Dokku/etc.
This template has a fully-automated dev setup for [Gitpod](https://docs.gitlab.com/ee/integration/gitpod.html). ### Requirements
If you open this project in Gitpod, you'll get all Node dependencies pre-installed and Express will open a web preview. - PostgreSQL (with an empty database created and an account to access it)
### Environment Variables
- `NODE_ENV` - set to `production`, `testing`, or `development`
- `PGHOST` - set to your database URL
- `PGPORT` - set to the database port
- `PGDATABASE` - set to the name of your database (i.e. `scoretrackerdb`)
- `PGUSER` - set to the user for managing the database
- `PGPASSWORD` - set to the password for that user
- `PUBLIC_SUBMIT_PAGE` (default: `false`) - set to `true` to allow score submissions without an account
## Code
This program uses Node.js/Express.js for the backend, PostgreSQL for the database (with node-postgres), and Passport.js for managing users and sessions.
To view the code, clone the repository and open it in VSCode/VSCodium.
### Structure
- `database` folder contains backend scripts for managing and storing data.
- `mail` folder (currenly unused) contains backend scripts for sending emails.
- `public` folder contains publically accessible scripts and stylesheets for frontend.
- `scripts` folder contains scripts used by specific webpages.
- `stylesheets` folder contains CSS for various webpages.
- `routes` folder contains various routes used by the program.
- `about.js` directs to the about page (`/about`).
- `auth.js` deals with logging in and out (`/auth/*`).
- `checkLoginStatus.js` contains functions for checking the login status of the current user.
- `data.js` sends various data to the client in JSON format (`/data/*`).
- `fetch.js` sends more specific data formatted for specific pages in JSON format (`/fetch/*`)
- `index.js` directs to the home page (`/`).
- `manage.js` contains various functions that allows the user to add and edit items through the web browser (`/manage/*`).
- `views` folder contains pug templates for each webpage, and a `layout` template for the base layout of each page.
- `.env.example` is a template for the environment variables in a development workspace. Rename to `.env` and change values as needed.

39
app.js
View File

@ -3,12 +3,42 @@ var express = require('express');
var path = require('path'); var path = require('path');
var cookieParser = require('cookie-parser'); var cookieParser = require('cookie-parser');
var logger = require('morgan'); var logger = require('morgan');
var random = require('./database/accounts/random');
const passport = require('passport');
const session = require('express-session');
const accounts = require('./database/accounts/accounts');
var flash = require('connect-flash');
var indexRouter = require('./routes/index'); var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users'); var dataRouter = require('./routes/data');
var manageRouter = require('./routes/manage');
var authRouter = require('./routes/auth');
var aboutRouter = require('./routes/about');
var fetchRouter = require('./routes/fetch');
var app = express(); var app = express();
// flash setup
app.use(flash());
// session setup
app.use(
session({
secret: random.makeid(20),
resave: false,
saveUninitialized: true,
})
);
// passport setup
app.use(passport.initialize());
app.use(passport.session());
//passport.use(accounts.createStrategy());
//passport.serializeUser(accounts.serializeUser());
//passport.deserializeUser(accounts.deserializeUser());
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug'); app.set('view engine', 'pug');
@ -20,7 +50,12 @@ app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); app.use('/', indexRouter);
app.use('/users', usersRouter); app.use('/data', dataRouter);
app.use('/manage', manageRouter);
app.use('/auth', authRouter);
app.use('/about', aboutRouter);
app.use('/fetch', fetchRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function(req, res, next) { app.use(function(req, res, next) {

View File

@ -0,0 +1,144 @@
const database = require('./../database');
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
class User {
constructor(id, email, isAdmin, name) {
this.id = id;
this.email = email;
this.isAdmin = isAdmin;
this.name = name;
}
}
async function checkForAdminAccount() {
const adminUsersQuery = `SELECT *
FROM accounts.users
WHERE admin = true;`;
const adminUsers = await database.executeQuery(adminUsersQuery);
if(adminUsers.length == 0) {
const passwordHash = await generateHash('admin');
const createTempAdminQuery = `INSERT INTO accounts.users(email, password, admin)
VALUES('admin@example.com', $1, true);`;
database.executeQuery(createTempAdminQuery, [passwordHash]);
console.log("Created temp admin account 'admin@example.com' with password 'admin'.");
}
}
database.initializationStatus.then(() => checkForAdminAccount());
passport.use(new localStrategy({
usernameField: 'email',
passwordField: 'password'},
(username, password, cb) => {
query = `SELECT user_id, email, password, admin
FROM accounts.users
WHERE email = $1`;
database.executeQuery(query, [username])
.then(result => {
if(result.length > 0) {
const first = result[0];
const matches = bcrypt.compareSync(password, first[2]);
if(matches) {
return cb(null, { id: first[0], email: first[1], admin: first[3] })
}
else
{
return cb(null, false)
}
} else {
return cb(null, false)
}
});
}));
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, cb) => {
query = `SELECT user_id, email, admin
FROM accounts.users
WHERE user_id = $1`;
database.executeQuery(query, [parseInt(id, 10)])
.then(result => {
cb(null, result[0]);
});
});
async function generateHash(password) {
const salt = bcrypt.genSaltSync();
return bcrypt.hashSync(password, salt);
}
async function create(email, password, isAdmin, name) {
const hash = await generateHash(password);
const query = `INSERT INTO accounts.users(email, password, admin, full_name)
VALUES($1, $2, $3, $4)`;
await database.executeQuery(query, [email, hash, isAdmin, name]);
}
async function edit(id, email, password, isAdmin, name) {
if(password) {
const hash = await generateHash(password);
const query = `UPDATE accounts.users
SET email = $2,
password = $3,
admin = $4,
full_name = $5
WHERE user_id = $1;`;
await database.executeQuery(query, [id, email, hash, isAdmin, name]);
} else {
const query = `UPDATE accounts.users
SET email = $2,
admin = $3,
full_name = $4
WHERE user_id = $1;`;
await database.executeQuery(query, [id, email, isAdmin, name]);
}
return new User(id, email, isAdmin, name);
}
async function remove(id) {
const query = `DELETE FROM accounts.users
WHERE user_id = $1
RETURNING email, admin, full_name;`;
const row = (await database.executeQuery(query, [id]))[0];
return new User(id, row[0], row[1], row[2]);
}
async function retrieveAll() {
const query = `SELECT user_id, email, admin, full_name
FROM accounts.users
ORDER BY full_name;`;
const table = await database.executeQuery(query);
const accountsList = [];
table.forEach((row) => {
accountsList.push(new User(row[0], row[1], row[2], row[3]));
});
return accountsList;
}
async function getFromID(id) {
const query = `SELECT user_id, email, admin, full_name
FROM accounts.users
WHERE user_id = $1;`;
const row = (await database.executeQuery(query, [id]))[0];
return new User(id, row[1], row[2], row[3]);
}
exports.create = create;
exports.edit = edit;
exports.remove = remove;
exports.retrieveAll = retrieveAll;
exports.getFromID = getFromID;
exports.passport = passport;

View File

@ -0,0 +1,12 @@
function makeid(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
exports.makeid = makeid;

View File

@ -0,0 +1,69 @@
const app = require('../app');
const { Client } = require('pg');
const fs = require('fs');
if (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV !== 'testing') {
require('dotenv').config();
}
const client = new Client();
client.connect();
async function executeQuery(query, values = []) {
const result = await client.query({
rowMode: 'array',
text: query,
values: values
});
return result.rows;
}
async function Initialize() {
console.log("Initializing database...")
const sql = fs.readFileSync('database/init_database.sql').toString();
await executeQuery(sql);
console.log("Database initialized.")
}
async function checkForDatabaseInitialization() {
const databaseIsSetupQuery = `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'scores'`;
let result = await executeQuery(databaseIsSetupQuery);
const databaseIsSetup = result.length !== 0;
if(!databaseIsSetup) {
await Initialize();
}
let latestMigration;
try {
const latestMigrationQuery = `SELECT value FROM metadata WHERE property = 'latest_migration';`;
latestMigration = +((await executeQuery(latestMigrationQuery))[0][0]);
} catch {
latestMigration = 0;
}
await performMigrations(latestMigration);
}
const initializationStatus = checkForDatabaseInitialization();
async function performMigrations(currentMigration) {
const migrationFileList = fs.readdirSync('database/migrations');
const latestMigration = +migrationFileList[migrationFileList.length - 1].slice(0, 1);
for(let i = +currentMigration + 1; i <= latestMigration; i++) {
const sql = fs.readFileSync(`database/migrations/${i}.sql`).toString();
await executeQuery(sql);
console.log(`Performed database migration ${i}`);
}
}
exports.executeQuery = executeQuery;
exports.initializationStatus = initializationStatus;

View File

@ -0,0 +1,124 @@
/* SCORE TRACKER DATABASE LAYOUT
scores:
sports:
*sport_id* | sport_name | currently_active
divisions:
*division_id* | division_name | gender | *sport_id* | currently_active
teams:
*team_id* | team_name | ~sport_id~ | currently_active
seasons:
*season_id* | school_year
games:
*game_id* | ~division_id~ | ~season_id~ | game_date | ~team1_id~ | ~team2_id~ | team1_score | team2_score | ~submitter_id~ | updated_timestamp | submitter_name
accounts:
users:
*user_id* | email | password | admin | full_name
*/
BEGIN;
CREATE SCHEMA IF NOT EXISTS accounts;
CREATE TABLE IF NOT EXISTS accounts.users(
user_id BIGINT GENERATED ALWAYS AS IDENTITY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
admin BOOLEAN NOT NULL DEFAULT FALSE,
full_name TEXT NOT NULL,
PRIMARY KEY(user_id)
);
CREATE SCHEMA IF NOT EXISTS scores;
CREATE TABLE IF NOT EXISTS scores.sports(
sport_id BIGINT GENERATED ALWAYS AS IDENTITY,
sport_name TEXT UNIQUE NOT NULL,
currently_active BOOLEAN DEFAULT TRUE,
PRIMARY KEY(sport_id)
);
CREATE TABLE IF NOT EXISTS scores.divisions(
division_id BIGINT GENERATED ALWAYS AS IDENTITY,
division_name TEXT NOT NULL,
gender VARCHAR(1) NOT NULL CHECK (gender IN ( 'F', 'M' ) ),
sport_id BIGINT NOT NULL,
currently_active BOOLEAN DEFAULT TRUE,
PRIMARY KEY(division_id),
CONSTRAINT fk_sport
FOREIGN KEY(sport_id)
REFERENCES scores.sports(sport_id)
);
CREATE TABLE IF NOT EXISTS scores.teams(
team_id BIGINT GENERATED ALWAYS AS IDENTITY,
team_name TEXT NOT NULL,
sport_id BIGINT NOT NULL,
currently_active BOOLEAN DEFAULT TRUE,
PRIMARY KEY(team_id),
CONSTRAINT fk_sport
FOREIGN KEY(sport_id)
REFERENCES scores.sports(sport_id)
);
CREATE TABLE IF NOT EXISTS scores.seasons(
season_id BIGINT GENERATED ALWAYS AS IDENTITY,
school_year INTEGER NOT NULL,
PRIMARY KEY(season_id)
);
CREATE TABLE IF NOT EXISTS scores.games(
game_id BIGINT GENERATED ALWAYS AS IDENTITY,
division_id BIGINT NOT NULL,
season_id BIGINT NOT NULL,
game_date DATE NOT NULL,
team1_id BIGINT NOT NULL,
team2_id BIGINT NOT NULL,
team1_score INTEGER NOT NULL,
team2_score INTEGER NOT NULL,
submitter_name TEXT,
submitter_id BIGINT,
updated_timestamp TIMESTAMP WITH TIME ZONE DEFAULT now(),
PRIMARY KEY(game_id),
CONSTRAINT fk_division
FOREIGN KEY(division_id)
REFERENCES scores.divisions(division_id),
CONSTRAINT fk_season
FOREIGN KEY(season_id)
REFERENCES scores.seasons(season_id),
CONSTRAINT fk_team1
FOREIGN KEY(team1_id)
REFERENCES scores.teams(team_id),
CONSTRAINT fk_team2
FOREIGN KEY(team2_id)
REFERENCES scores.teams(team_id),
CONSTRAINT fk_submitter
FOREIGN KEY(submitter_id)
REFERENCES accounts.users(user_id)
);
CREATE TABLE IF NOT EXISTS metadata(
property TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
INSERT INTO metadata(property, value)
VALUES("latest_migration", "3");
COMMIT;

View File

@ -0,0 +1,13 @@
/* ADD METADATA TABLE */
BEGIN;
CREATE TABLE IF NOT EXISTS metadata(
property TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
INSERT INTO metadata(property, value)
VALUES('latest_migration', '1');
COMMIT;

View File

@ -0,0 +1,12 @@
/* ADD ACCOUNT NAME COLUMN */
BEGIN;
ALTER TABLE accounts.users
ADD COLUMN full_name TEXT;
UPDATE metadata
SET value = '2'
WHERE property = 'latest_migration';
COMMIT;

View File

@ -0,0 +1,15 @@
/* ADD OPTIONAL SUBMITTER NAME COLUMN IN GAMES TABLE */
BEGIN;
ALTER TABLE scores.games ALTER COLUMN submitter_id DROP NOT NULL;
ALTER TABLE scores.games
ADD COLUMN submitter_name TEXT;
UPDATE metadata
SET value = '3'
WHERE property = 'latest_migration';
COMMIT;

View File

@ -0,0 +1,99 @@
const database = require('./../database');
const genders = require('./genders');
class Division {
constructor(id, name, gender, sportID) {
this.id = id;
this.name = name;
this.gender = gender;
this.sportID = sportID;
}
}
function getGenderID(gender) {
return (gender == genders.MALE) ? "M" : "F";
}
function getGenderFromID(genderID) {
return (genderID == "F") ? genders.FEMALE : genders.MALE;
}
async function add(name, gender, sportID) {
const query = `INSERT INTO scores.divisions(division_name,gender,sport_id)
VALUES($1,$2,$3)
RETURNING division_id;`;
const genderID = getGenderID(gender);
const id = (await database.executeQuery(query, [name, genderID, sportID]))[0][0];
return new Division(id, name);
}
async function rename(id, name) {
const query = `UPDATE scores.divisions
SET division_name = $2
WHERE division_id = $1;`;
await database.executeQuery(query, [id, name]);
return new Division(id, name);
}
async function remove(id) {
const query = `DELETE FROM scores.divisions
WHERE division_id = $1
RETURNING division_name;`;
const name = (await database.executeQuery(query, [id]))[0][0];
return new Division(id, name);
}
async function retrieve(sportID = undefined, gender = undefined) {
let table;
if(sportID && gender) {
const query = `SELECT division_id, division_name, gender, sport_id
FROM scores.divisions
WHERE sport_id = $1 AND gender = $2
ORDER BY division_name;`;
const genderID = getGenderID(gender);
table = await database.executeQuery(query, [sportID, genderID]);
}
else {
const query = `SELECT division_id, division_name, gender, sport_id
FROM scores.divisions
ORDER BY sport_id,
division_name,
gender;`;
table = await database.executeQuery(query);
}
const divisionsList = [];
table.forEach((row) => {
divisionsList.push(new Division(row[0], row[1], getGenderFromID(row[2]), row[3]));
});
return divisionsList;
}
async function getFromID(id) {
const query = `SELECT division_id, division_name, gender, sport_id
FROM scores.divisions
WHERE division_id = $1;`;
const row = (await database.executeQuery(query, [id]))[0];
return new Division(id, row[1], getGenderFromID(row[2]), row[3]);
}
exports.add = add;
exports.rename = rename;
exports.remove = remove;
exports.retrieve = retrieve;
exports.getFromID = getFromID;

View File

@ -0,0 +1,140 @@
const database = require('./../database');
class Game {
constructor(id, date, team1ID, team2ID, team1Score, team2Score, divisionID, seasonID, submitterID, submitterName) {
this.id = id;
this.date = date;
this.team1ID = team1ID;
this.team2ID = team2ID;
this.team1Score = team1Score;
this.team2Score = team2Score;
this.divisionID = divisionID;
this.seasonID = seasonID;
this.submitterID = submitterID;
this.submitterName = submitterName;
}
}
async function add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, submitterID, submitterName = undefined) {
let id;
if(submitterName) {
const query = `INSERT INTO scores.games(division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_name)
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING game_id;`;
id = (await database.executeQuery(query, [divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, submitterName]))[0][0];
} else {
const query = `INSERT INTO scores.games(division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id)
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING game_id;`;
id = (await database.executeQuery(query, [divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, submitterID]))[0][0];
}
return new Game(id, date, team1ID, team2ID, team1Score, team2Score);
}
async function remove(id) {
const query = `DELETE FROM scores.games
WHERE game_id = $1
RETURNING * ;`;
const row = (await database.executeQuery(query, [id]))[0];
return new Game(id, row[3], row[4], row[5], row[6], row[7]);
}
async function retrieve(teamID, divisionID, seasonID) {
let table;
if(teamID && divisionID && seasonID) {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id, submitter_name
FROM scores.games
WHERE (team1_id = $1 OR team2_id = $1) AND division_id = $2 AND season_id = $3
ORDER BY game_date DESC;`;
table = await database.executeQuery(query, [teamID,divisionID,seasonID]);
}
else {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id, submitter_name
FROM scores.games
ORDER BY game_date DESC;`;
table = await database.executeQuery(query);
}
const gamesList = [];
table.forEach((row) => {
if(teamID) {
const opponentIsTeam2 = teamID != row[5];
const opponentID = opponentIsTeam2 ? row[5] : row[4];
const teamScore = opponentIsTeam2 ? row[6] : row[7];
const opponentScore = opponentIsTeam2 ? row[7] : row[6];
gamesList.push(new Game(row[0], row[3].toISOString().slice(0,10), teamID, opponentID, teamScore, opponentScore, row[1], row[2], row[8], row[9]));
}
else {
gamesList.push(new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2], row[8], row[9]));
}
});
return gamesList;
}
async function retrieveByUser(userID) {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score
FROM scores.games
WHERE submitter_id = $1
ORDER BY game_date DESC;`;
const table = await database.executeQuery(query, [userID]);
const gamesList = [];
table.forEach((row) => {
gamesList.push(new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2]));
});
return gamesList;
}
async function edit(gameID, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score) {
const query = `UPDATE scores.games
SET division_id = $2,
season_id = $3,
game_date = $4,
team1_id = $5,
team2_id = $6,
team1_score = $7,
team2_score = $8
WHERE game_id = $1;`;
await database.executeQuery(query, [gameID, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score]);
return new Game(gameID, date, team1ID, team2ID, team1Score, team2Score, divisionID, seasonID);
}
async function getFromID(gameID) {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id
FROM scores.games
WHERE game_id = $1;`;
const row = (await database.executeQuery(query, [gameID]))[0];
return new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2], row[8]);
}
async function getLatest(userID = undefined) {
if(userID) {
const games = await retrieveByUser(userID);
return games[0];
} else {
const games = await retrieve();
return games[0];
}
}
exports.add = add;
exports.remove = remove;
exports.retrieve = retrieve;
exports.retrieveByUser = retrieveByUser;
exports.edit = edit;
exports.getFromID = getFromID;
exports.getLatest = getLatest;

View File

@ -0,0 +1,48 @@
const database = require('./../database');
class Gender {
constructor(name) {
this.name = name;
}
}
const MALE = new Gender("male");
const FEMALE = new Gender("female");
async function retrieveBySport(sportID) {
const query = `SELECT DISTINCT(gender)
FROM scores.divisions
WHERE sport_id = $1;`;
const table = await database.executeQuery(query, [sportID]);
const gendersList = [];
if(table.length == 0) {
return gendersList;
}
if(table.length == 2) {
gendersList.push(FEMALE);
gendersList.push(MALE);
return gendersList;
}
else if(table[0][0] = "F") {
gendersList.push(FEMALE);
return gendersList;
}
else if(table[0][0] = "M") {
gendersList.push(MALE);
return gendersList;
}
}
exports.MALE = MALE;
exports.FEMALE = FEMALE;
exports.retrieveBySport = retrieveBySport;

View File

@ -0,0 +1,51 @@
const database = require('./../database');
class Season {
constructor(id, year) {
this.id = id;
this.year = year;
}
}
async function add(year) {
const query = `INSERT INTO scores.seasons(school_year)
VALUES($1)
RETURNING season_id;`;
const id = (await database.executeQuery(query, [year]))[0][0];
return new Season(id, year);
}
async function remove(id) {
const query = `DELETE FROM scores.seasons
WHERE season_id = $1
RETURNING school_year;`;
const year = (await database.executeQuery(query, [id]))[0][0];
return new Season(id, year);
}
async function retrieveAll() {
const query = `SELECT *
FROM scores.seasons
ORDER BY school_year DESC;`;
const table = await database.executeQuery(query);
const seasonsList = [];
table.forEach((row) => {
seasonsList.push(new Season(row[0], row[1]));
});
return seasonsList;
}
exports.add = add;
exports.remove = remove;
exports.retrieveAll = retrieveAll;

View File

@ -0,0 +1,67 @@
const database = require('./../database');
class Sport {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
async function add(name) {
const query = `INSERT INTO scores.sports(sport_name)
VALUES($1)
RETURNING sport_id;`;
const id = (await database.executeQuery(query, [name]))[0][0];
return new Sport(id, name);
}
async function rename(id, name) {
const query = `UPDATE scores.sports
SET sport_name = $2
WHERE sport_id = $1;`;
await database.executeQuery(query, [id, name]);
return new Sport(id, name);
}
async function remove(id) {
const query = `DELETE FROM scores.sports
WHERE sport_id = $1
RETURNING sport_name;`;
const name = (await database.executeQuery(query, [id]))[0][0];
return new Sport(id, name);
}
async function retrieveAll() {
const query = `SELECT *
FROM scores.sports
ORDER BY sport_name;`;
const table = await database.executeQuery(query);
const sportsList = [];
table.forEach((row) => {
sportsList.push(new Sport(row[0], row[1]));
});
return sportsList;
}
async function getFromID(id) {
const query = `SELECT sport_name
FROM scores.sports
WHERE sport_id = $1;`;
const name = (await database.executeQuery(query, [id]))[0][0];
return new Sport(id, name);
}
exports.add = add;
exports.rename = rename;
exports.remove = remove;
exports.retrieveAll = retrieveAll;
exports.getFromID = getFromID;

View File

@ -0,0 +1,83 @@
const database = require('./../database');
class Team {
constructor(id, name, sportID) {
this.id = id;
this.name = name;
this.sportID = sportID;
}
}
async function add(name, sportID) {
const query = `INSERT INTO scores.teams(team_name, sport_id)
VALUES($1, $2)
RETURNING team_id;`;
const id = (await database.executeQuery(query, [name, sportID]))[0][0];
return new Team(id, name);
}
async function rename(id, name) {
const query = `UPDATE scores.teams
SET team_name = $2
WHERE team_id = $1;`;
await database.executeQuery(query, [id, name]);
return new Team(id, name);
}
async function remove(id) {
const query = `DELETE FROM scores.teams
WHERE team_id = $1
RETURNING team_name;`;
const name = (await database.executeQuery(query, [id]))[0][0];
return new Team(id, name);
}
async function retrieve(sportID = undefined) {
let table;
if(sportID) {
const query = `SELECT team_id, team_name, sport_id
FROM scores.teams
WHERE sport_id = $1
ORDER BY team_name;`;
table = await database.executeQuery(query, [sportID]);
}
else {
const query = `SELECT team_id, team_name, sport_id
FROM scores.teams
ORDER BY
sport_id,
team_name;`;
table = await database.executeQuery(query);
}
const teamsList = [];
table.forEach((row) => {
teamsList.push(new Team(row[0], row[1], row[2]));
});
return teamsList;
}
async function getFromID(id) {
const query = `SELECT team_name, sport_id
FROM scores.teams
WHERE team_id = $1;`;
const row = (await database.executeQuery(query, [id]))[0];
return new Team(id, row[0], row[1]);
}
exports.add = add;
exports.rename = rename;
exports.remove = remove;
exports.retrieve = retrieve;
exports.getFromID = getFromID;

33
mail/mail.js 100644
View File

@ -0,0 +1,33 @@
const app = require('../app');
const nodemailer = require('nodemailer');
if (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV !== 'testing') {
require('dotenv').config();
}
module.exports = {
send: function (recipient, subject, message) {
send(recipient, subject, message);
}
};
var send = function (recipient, subject, message) {
transporter.sendMail({
to: recipient, // list of receivers
subject: subject, // Subject line
html: message, // html body
});
}
let transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: process.env.MAIL_SECURE,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
}, {
from: process.env.MAIL_FROM
});

3279
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,25 @@
{ {
"name": "demo", "name": "score-tracker",
"version": "0.0.0", "version": "1.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./bin/www"
"test": "mocha"
}, },
"dependencies": { "dependencies": {
"async": "^3.2.2",
"bcrypt": "^5.0.1",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^10.0.0",
"express": "~4.16.0", "express": "~4.16.0",
"express-session": "^1.17.2",
"http-errors": "~1.6.2", "http-errors": "~1.6.2",
"morgan": "~1.9.0", "morgan": "~1.9.0",
"pug": "2.0.0-beta11" "nodemailer": "^6.6.5",
}, "passport": "^0.5.0",
"devDependencies": { "passport-local": "^1.0.0",
"mocha": "^5.1.1", "pg": "^8.7.1",
"supertest": "^3.0.0" "pug": "^3.0.2"
} }
} }

View File

@ -0,0 +1,99 @@
export async function getSports() {
const response = await fetch(`/data/sports`);
const sportsList = await response.json();
return sportsList;
}
export async function getSportName(sportID) {
const response = await fetch(`/data/sport?sport=${sportID}`);
const sport = await response.json();
return sport.name;
}
export async function getSeasons() {
const response = await fetch(`/data/seasons`);
const seasonsList = await response.json();
return seasonsList;
}
export async function getGenders(sportID) {
const response = await fetch(`/data/genders?sport=${+sportID}`);
const gendersList = await response.json();
return gendersList;
}
export async function getDivisions(sportID = undefined, gender = undefined) {
let URL = '/data/divisions?';
if(sportID) URL += `sport=${+sportID}&`;
if(gender) URL += `gender=${gender}&`;
const response = await fetch(URL);
const divisionsList = await response.json();
return divisionsList;
}
export async function getDivision(divisionID) {
const response = await fetch(`/data/division?division=${divisionID}`);
const division = await response.json();
return division;
}
export async function getTeams(sportID = undefined) {
let URL = '/data/teams?';
if(sportID) URL += `sport=${+sportID}&`;
const response = await fetch(URL);
const teamsList = await response.json();
return teamsList;
}
export async function getTeam(teamID) {
const response = await fetch(`/data/team?team=${+teamID}`);
const team = await response.json();
return team;
}
export async function getGames(teamID = undefined, divisionID = undefined, seasonID = undefined) {
let URL = '/data/games?';
if(teamID) URL += `team=${+teamID}&`;
if(divisionID) URL += `division=${+divisionID}&`;
if(seasonID) URL += `season=${+seasonID}`;
const response = await fetch(URL);
const gamesList = await response.json();
return gamesList;
}
export async function getGamesByUser() {
let URL = '/data/games?user=1';
const response = await fetch(URL);
const gamesList = await response.json();
return gamesList;
}
export async function getGame(gameID) {
const response = await fetch(`/data/game?game=${gameID}`);
const game = await response.json();
return game;
}
export async function getLatestGame(ofUser = false) {
let URL = `/data/game?`;
if(ofUser) URL += `ofuser=1`;
const response = await fetch(URL);
const game = await response.json();
return game;
}
export async function getAccounts() {
const response = await fetch(`/data/accounts`);
const accounts = await response.json();
return accounts;
}
export async function getAccount(accountID) {
const response = await fetch(`/data/account?account=${accountID}`);
const account = await response.json();
return account;
}

View File

@ -0,0 +1,167 @@
import * as Data from "./data.js";
export async function populateSports(sportDropdown, selectedSportID = undefined, data = undefined) {
sportDropdown.innerHTML = "";
let sportsList;
if(data) {
sportsList = data.sports;
} else {
sportsList = await Data.getSports();
}
let currentIndex = 0;
let selectedSportIndex;
sportsList.forEach(sport => {
const option = document.createElement('option');
option.text = sport.name;
option.value = sport.id;
sportDropdown.appendChild(option);
if(sport.id == selectedSportID || (data && sport.id == data.latestGame.sportID)) selectedSportIndex = currentIndex;
currentIndex++;
});
if(selectedSportIndex) sportDropdown.selectedIndex = selectedSportIndex;
}
export async function populateSeasons(seasonDropdown, selectedSeasonID = undefined, data = undefined) {
seasonDropdown.innerHTML = "";
let seasonsList;
if(data) {
seasonsList = data.seasons;
} else {
seasonsList = await Data.getSeasons();
}
let currentIndex = 0;
let selectedSeasonIndex;
seasonsList.forEach(season => {
const option = document.createElement('option');
option.text = (season.year - 1) + "-" + season.year;
option.value = season.id;
seasonDropdown.appendChild(option);
if(season.id == selectedSeasonID || (data && season.id == data.latestGame.seasonID)) selectedSeasonIndex = currentIndex;
currentIndex++;
});
if(selectedSeasonIndex) seasonDropdown.selectedIndex = selectedSeasonIndex;
}
export async function populateGenders(genderDropdown, selectedSportID, selectedGender = undefined, data = undefined) {
genderDropdown.innerHTML = "";
let gendersList;
if(data) {
gendersList = data.genders;
selectedSportID = data.latestGame.sportID;
} else {
gendersList = await Data.getGenders(selectedSportID);
}
if(selectedSportID) {
let currentIndex = 0;
let selectedGenderIndex;
gendersList.forEach(gender => {
const option = document.createElement('option');
option.text = (gender.name == "female") ? "Female" : (gender.name == "male") ? "Male" : "";
option.value = gender.name;
genderDropdown.appendChild(option);
if(gender.name == selectedGender || (data && gender.name == data.latestGame.gender.name)) selectedGenderIndex = currentIndex;
currentIndex++;
});
if(selectedGenderIndex) genderDropdown.selectedIndex = selectedGenderIndex;
}
}
export async function populateDivisions (divisionDropdown, selectedSportID, selectedGender, selectedDivisionID = undefined, data = undefined) {
divisionDropdown.innerHTML = "";
if(data) {
selectedSportID = data.latestGame.sportID;
selectedGender = data.latestGame.gender;
}
if(selectedSportID && selectedGender) {
let divisionsList;
if(data) {
divisionsList = data.divisions;
} else {
divisionsList = await Data.getDivisions(selectedSportID, selectedGender);
}
let currentIndex = 0;
let selectedDivisionIndex;
divisionsList.forEach(division => {
const option = document.createElement('option');
option.text = division.name;
option.value = division.id;
divisionDropdown.appendChild(option);
if(division.id == selectedDivisionID || (data && division.id == data.latestGame.divisionID)) selectedDivisionIndex = currentIndex;
currentIndex++;
});
if(selectedDivisionIndex) divisionDropdown.selectedIndex = selectedDivisionIndex;
}
}
export async function populateTeams(teamDropdown, selectedSportID, selectedTeamID = undefined, data = undefined, useOpponent = false) {
teamDropdown.innerHTML = "";
if(data) {
selectedSportID = data.latestGame.sportID;
}
if(selectedSportID) {
let teamsList;
if(data) {
teamsList = data.teams;
} else {
teamsList = await Data.getTeams(selectedSportID);
}
let currentIndex = 0;
let selectedTeamIndex;
if(data) {
selectedTeamID = useOpponent ? data.latestGame.team2ID : data.latestGame.team1ID;
}
teamsList.forEach(team => {
const option = document.createElement('option');
option.text = team.name;
option.value = team.id;
teamDropdown.appendChild(option);
if(team.id == selectedTeamID) selectedTeamIndex = currentIndex;
currentIndex++;
});
if(selectedTeamIndex) teamDropdown.selectedIndex = selectedTeamIndex;
}
}
export async function addHiddenValue(name, value, form) {
const valueInput = document.createElement('input');
valueInput.setAttribute('name', name);
valueInput.setAttribute('value', value);
valueInput.setAttribute('type', 'hidden');
form.appendChild(valueInput);
}
export async function addRemoveFunction(removeButton, form, objectTitle) {
removeButton.addEventListener('click', async () => {
const verified = confirm(`This ${objectTitle} will be removed.`);
if(verified) {
await addHiddenValue('remove', 1, form);
form.submit();
}
});
}

View File

@ -0,0 +1,135 @@
import * as Data from "./data.js";
import * as Form from "./form.js";
const dropdownsDiv = document.getElementById('dropdowns-div');
const sportDropdown = document.getElementById('sport-dropdown');
const seasonDropdown = document.getElementById('year-dropdown');
const genderDropdown = document.getElementById('gender-dropdown');
const divisionDropdown = document.getElementById('division-dropdown');
const teamDropdown = document.getElementById('team-dropdown');
const gamesTable = document.getElementById('games-table');
const gamesTableHeader = document.getElementById('games-table-header');
const noScoresMessage = document.getElementById('no-scores-message');
const addScoreButton = document.getElementById('add-score-button');
const manageButton = document.getElementById('manage-button');
const loadingSpan = document.getElementById('loading');
async function initializeForm() {
const data = await (await fetch(`/fetch/index/dropdown`)).json();
await Form.populateSeasons(seasonDropdown, null, data);
await Form.populateSports(sportDropdown, null, data);
await Form.populateGenders(genderDropdown, null, null, data);
await Form.populateDivisions(divisionDropdown, null, null, null, data);
await Form.populateTeams(teamDropdown, null, null, data);
seasonDropdown.onchange = loadTable;
sportDropdown.onchange = async () => {
await Form.populateGenders(genderDropdown, sportDropdown.value)
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
await Form.populateTeams(teamDropdown, sportDropdown.value);
loadTable();
};
genderDropdown.onchange = async () => {
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
loadTable();
};
divisionDropdown.onchange = loadTable;
teamDropdown.onchange = loadTable;
loadingSpan.textContent = '';
dropdownsDiv.style.visibility = 'visible';
loadTable();
}
initializeForm();
async function loadTable() {
gamesTable.innerHTML = "";
gamesTableHeader.textContent = "";
noScoresMessage.textContent = "";
const selectedTeamID = teamDropdown.value;
const selectedDivisionID = divisionDropdown.value;
const selectedSeasonID = seasonDropdown.value;
if(selectedTeamID && selectedDivisionID) {
gamesTableHeader.textContent = `Scores for ${teamDropdown.options[teamDropdown.selectedIndex].text}`;
noScoresMessage.textContent = "Loading scores...";
const requestURL = `/fetch/index/scores?team=${+selectedTeamID}&division=${+selectedDivisionID}&season=${+selectedSeasonID}`;
const gamesList = await (await fetch(requestURL)).json();
noScoresMessage.textContent = "";
if(gamesList.length > 0) {
await setupGamesTableHeaders();
gamesList.forEach((game) => {
const row = document.createElement('tr');
const scoreCell = document.createElement('td');
const winLossLine = document.createElement('span');
winLossLine.textContent = (game.team1Score > game.team2Score) ? "Win" : (game.team1Score < game.team2Score) ? "Loss" : "Tie";
scoreCell.appendChild(winLossLine);
const scoreLine = document.createElement('span');
scoreLine.textContent = game.team1Score + "-" + game.team2Score;
scoreCell.appendChild(scoreLine);
row.appendChild(scoreCell);
const opponentCell = document.createElement('td');
opponentCell.textContent = game.opponent.name;
row.appendChild(opponentCell);
const dateCell = document.createElement('td');
dateCell.textContent = game.date;
dateCell.style['white-space'] = 'nowrap';
row.appendChild(dateCell);
gamesTable.appendChild(row);
});
}
else {
noScoresMessage.textContent = "No scores available";
}
}
}
async function setupGamesTableHeaders() {
const row = document.createElement('tr');
const scoresHeader = document.createElement('th');
scoresHeader.textContent = "Score"
row.appendChild(scoresHeader);
const opponentHeader = document.createElement('th');
opponentHeader.textContent = "Opponent";
row.appendChild(opponentHeader);
const dateHeader = document.createElement('th');
dateHeader.textContent = "Date";
row.appendChild(dateHeader);
gamesTable.appendChild(row);
}
if(addScoreButton) {
addScoreButton.addEventListener('click', () => {
window.location.href = '/manage/game';
});
}
if(manageButton) {
manageButton.addEventListener('click', () => {
window.location.href = '/manage'
});
}

View File

@ -0,0 +1,21 @@
const logInButton = document.getElementById('login-button');
const logOutButton = document.getElementById('logout-button');
const homeButton = document.getElementById('home-button');
if(logInButton) {
logInButton.addEventListener('click', () => {
window.location.href = "/auth/login";
});
}
if(logOutButton) {
logOutButton.addEventListener('click', () => {
window.location.href = "/auth/logout";
});
}
if(homeButton) {
homeButton.addEventListener('click', () => {
window.location.href = '/';
});
}

View File

@ -0,0 +1,410 @@
import * as Data from "./data.js";
const categoryDropdown = document.getElementById('category-dropdown');
const itemsListTable = document.getElementById('items-list');
const addNewButton = document.getElementById('add-new-button');
const loadingSpan = document.getElementById('loading-message');
function getGenderLetter(genderName) {
return genderName == "female" ? "F" : "M";
}
class Category {
constructor(name, getItems, listHeaders, listItem, addItem, editItem) {
this.name = name;
this.getItems = getItems;
this.listHeaders = listHeaders;
this.listItem = listItem;
this.addItem = addItem;
this.editItem = editItem;
}
}
const CATEGORIES = [];
CATEGORIES.push(new Category(
"seasons",
async function getSeasons() {
return await Data.getSeasons();
},
async function listSeasonHeaders() {
const headerRow = document.createElement('tr');
const yearsHeader = document.createElement('th');
yearsHeader.textContent = "Years";
headerRow.appendChild(yearsHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
itemsListTable.appendChild(headerRow);
},
function listSeason(season, row) {
const yearCell = document.createElement('td');
yearCell.textContent = (season.year - 1) + "-" + season.year;
row.appendChild(yearCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
},
async function addSeason() {
window.location.href = "/manage/season";
},
async function editSeason(id) {
const verified = confirm(`This season will be removed.`);
if(verified) {
const form = document.createElement('form');
form.action = "/manage/season";
form.method = "POST";
form.style.visibility = "hidden";
itemsListTable.appendChild(form);
const remove = document.createElement('input');
remove.setAttribute('name', 'remove');
remove.setAttribute('value', 1);
form.appendChild(remove);
const seasonID = document.createElement('input');
seasonID.setAttribute('name', 'season');
seasonID.setAttribute('value', id);
form.appendChild(seasonID);
form.submit();
}
}
));
CATEGORIES.push(new Category(
"sports",
async function getSports() {
return await Data.getSports();
},
async function listSportHeaders() {
const headerRow = document.createElement('tr');
const nameHeader = document.createElement('th');
nameHeader.textContent = "Name";
headerRow.appendChild(nameHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
itemsListTable.appendChild(headerRow);
},
function listSport(sport, row) {
const nameCell = document.createElement('td');
nameCell.textContent = sport.name;
row.appendChild(nameCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
},
async function addSport() {
window.location.href = `/manage/sport`;
},
async function editSport(id) {
window.location.href = `/manage/sport?sport=${id}`
}
));
CATEGORIES.push(new Category(
"divisions",
async function getDivisions() {
return await (await fetch('/fetch/manage/divisions')).json();
},
async function listDivisionHeaders() {
const headerRow = document.createElement('tr');
const nameHeader = document.createElement('th');
nameHeader.textContent = "Name";
headerRow.appendChild(nameHeader);
const genderHeader = document.createElement('th');
headerRow.appendChild(genderHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const sportHeader = document.createElement('th');
sportHeader.textContent = "Sport";
headerRow.appendChild(sportHeader);
itemsListTable.appendChild(headerRow);
},
function listDivision(division, row) {
const nameCell = document.createElement('td');
nameCell.textContent = division.name;
row.appendChild(nameCell);
const genderCell = document.createElement('td');
const gender = getGenderLetter(division.gender.name);
genderCell.textContent = gender;
row.appendChild(genderCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const sportCell = document.createElement('td');
let sportName = division.sport.name;
sportCell.textContent = sportName;
row.appendChild(sportCell);
},
async function addDivision() {
window.location.href = "/manage/division";
},
async function editDivision(id) {
window.location.href = `/manage/division?division=${id}`
}
));
CATEGORIES.push(new Category(
"teams",
async function getTeams() {
return await (await fetch('/fetch/manage/teams')).json();
},
async function listTeamHeaders() {
const headerRow = document.createElement('tr');
const nameHeader = document.createElement('th');
nameHeader.textContent = "Name";
headerRow.appendChild(nameHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const sportHeader = document.createElement('th');
sportHeader.textContent = "Sport";
headerRow.appendChild(sportHeader);
itemsListTable.appendChild(headerRow);
},
function listTeam(team, row) {
const nameCell = document.createElement('td');
nameCell.textContent = team.name;
row.appendChild(nameCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const sportCell = document.createElement('td');
let sportName = team.sport.name;
sportCell.textContent = sportName;
row.appendChild(sportCell);
},
async function addTeam() {
window.location.href = "/manage/team";
},
async function editTeam(id) {
window.location.href = `/manage/team?team=${id}`;
}
));
CATEGORIES.push(new Category(
"games",
async function getGames() {
return await (await fetch('/fetch/manage/games')).json();
},
async function listGameHeaders() {
const headerRow = document.createElement('tr');
const teamsHeader = document.createElement('th');
teamsHeader.textContent = "Teams";
headerRow.appendChild(teamsHeader);
const scoreHeader = document.createElement('th');
headerRow.appendChild(scoreHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const sportNameHeader = document.createElement('th');
sportNameHeader.textContent = "Sport";
headerRow.appendChild(sportNameHeader);
const dateHeader = document.createElement('th');
dateHeader.textContent = "Date";
headerRow.appendChild(dateHeader);
const submitterHeader = document.createElement('th');
submitterHeader.textContent = "Submitter";
headerRow.appendChild(submitterHeader);
itemsListTable.appendChild(headerRow);
},
function listGame(game, row) {
const teamsCell = document.createElement('td');
const team1NameSpan = document.createElement('span');
let team1Name = game.team1.name;
team1NameSpan.textContent = team1Name;
teamsCell.appendChild(team1NameSpan);
const team2NameSpan = document.createElement('span');
let team2Name = game.team2.name;
team2NameSpan.textContent = team2Name;
teamsCell.appendChild(team2NameSpan);
row.appendChild(teamsCell);
const scoresCell = document.createElement('td');
const team1ScoreSpan = document.createElement('span');
team1ScoreSpan.textContent = game.team1Score;
scoresCell.appendChild(team1ScoreSpan);
const team2ScoreSpan = document.createElement('span');
team2ScoreSpan.textContent = game.team2Score;
scoresCell.appendChild(team2ScoreSpan);
row.appendChild(scoresCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const sportCell = document.createElement('td');
const sportSpan = document.createElement('span');
const divisionSpan = document.createElement('span');
divisionSpan.classList.add('flat-content');
const divisionNameSpan = document.createElement('span');
const divisionGenderSpan = document.createElement('span');
divisionSpan.appendChild(divisionNameSpan);
divisionSpan.appendChild(divisionGenderSpan);
let divisionName = game.division.name;
let sportName = game.sport.name;
divisionNameSpan.textContent = divisionName;
sportSpan.textContent = sportName;
divisionGenderSpan.textContent = getGenderLetter(game.division.gender.name);
sportCell.appendChild(sportSpan);
sportCell.appendChild(divisionSpan);
row.appendChild(sportCell);
const dateCell = document.createElement('td');
const yearSpan = document.createElement('span');
yearSpan.textContent = game.date.slice(0,4);
dateCell.appendChild(yearSpan);
const dateSpan = document.createElement('span');
dateSpan.textContent = game.date.slice(5);
dateCell.appendChild(dateSpan);
row.appendChild(dateCell);
const submitterCell = document.createElement('td');
if(game.submitterID) {
let submitterName = game.submitter.name;
submitterCell.textContent = submitterName;
} else {
submitterCell.textContent = game.submitterName;
}
row.appendChild(submitterCell);
},
async function addGame() {
window.location.href = "/manage/game";
},
async function editGame(id) {
window.location.href = `/manage/game?game=${id}`;
}
));
CATEGORIES.push(new Category(
"accounts",
async function getAccounts() {
return await Data.getAccounts();
},
async function listAccountHeaders() {
const headerRow = document.createElement('tr');
const nameHeader = document.createElement('th');
nameHeader.textContent = "Name";
headerRow.appendChild(nameHeader);
const emailHeader = document.createElement('th');
emailHeader.textContent = "Email";
headerRow.appendChild(emailHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const adminHeader = document.createElement('th');
adminHeader.textContent = "Admin?";
headerRow.appendChild(adminHeader);
itemsListTable.appendChild(headerRow);
},
function listAccount(account, row) {
const nameCell = document.createElement('td');
nameCell.textContent = account.name;
row.appendChild(nameCell);
const emailCell = document.createElement('td');
emailCell.textContent = account.email;
row.appendChild(emailCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const adminCell = document.createElement('td');
adminCell.textContent = account.isAdmin;
row.appendChild(adminCell);
},
async function addAccount() {
window.location.href = "/manage/account";
},
async function editAccount(id) {
window.location.href = `/manage/account?account=${id}`;
}
));
async function listItems(category) {
loadingSpan.textContent = "Loading...";
itemsListTable.innerHTML = "";
const itemsList = await category.getItems();
await category.listHeaders();
itemsList.forEach(item => {
const row = document.createElement('tr');
category.listItem(item, row);
const manageCell = document.createElement('td');
const editSpan = document.createElement('span');
const editButton = document.createElement('button');
editButton.textContent = "✎";
editButton.style['font-size'] = '1.25em';
editButton.addEventListener('click', () => {
CATEGORIES[categoryDropdown.selectedIndex].editItem(item.id);
});
editSpan.appendChild(editButton);
manageCell.appendChild(editSpan);
row.appendChild(manageCell);
itemsListTable.appendChild(row);
});
loadingSpan.textContent = '';
}
if(window.location.hash) {
let correctIndex;
let index = 0;
CATEGORIES.forEach(category => {
if(window.location.hash == '#' + category.name) correctIndex = index;
index++;
})
if(correctIndex || correctIndex === 0) categoryDropdown.selectedIndex = correctIndex;
}
listItems(CATEGORIES[categoryDropdown.selectedIndex]);
categoryDropdown.onchange = () => {
listItems(CATEGORIES[categoryDropdown.selectedIndex]);
};
addNewButton.addEventListener('click', () => CATEGORIES[categoryDropdown.selectedIndex].addItem());

View File

@ -0,0 +1,67 @@
import * as Data from "../data.js";
import * as Form from "../form.js";
const submissionForm = document.getElementById('submission-form');
const nameTextbox = document.getElementById('name-textbox');
const emailTextbox = document.getElementById('email-textbox');
const passwordTextbox = document.getElementById('password-textbox');
const adminCheckboxSection = document.getElementById('admin-checkbox-section');
const adminCheckbox = document.getElementById('admin-checkbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
const loadingSpan = document.getElementById('loading-message');
async function Initialize() {
let params = new URLSearchParams(location.search);
let accountID = params.get('account') || (document.getElementById('account-id') ? document.getElementById('account-id').value : null);
if(accountID) {
const account = await Data.getAccount(accountID);
nameTextbox.value = account.name;
emailTextbox.value = account.email;
passwordTextbox.placeholder = "leave unchanged";
adminCheckbox.checked = account.isAdmin;
if(!document.getElementById('account-id')) {
adminCheckboxSection.style.visibility = "visible";
adminCheckbox.disabled = false;
Form.addHiddenValue('account', accountID, submissionForm);
}
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
}
else
{
adminCheckboxSection.style.visibility = "visible";
adminCheckbox.disabled = false;
}
nameTextbox.disabled = false;
nameTextbox.addEventListener('keyup', checkDataValidity);
emailTextbox.disabled = false;
emailTextbox.addEventListener('keyup', checkDataValidity);
passwordTextbox.disabled = false;
passwordTextbox.addEventListener('keyup', checkDataValidity);
checkDataValidity();
loadingSpan.textContent = '';
submissionForm.style.visibility = 'visible';
}
Initialize();
async function checkDataValidity() {
let dataIsValid = true;
if(!passwordTextbox.value && !passwordTextbox.placeholder) dataIsValid = false;
if(!nameTextbox.value) dataIsValid = false;
if(!emailTextbox.value) dataIsValid = false;
if(dataIsValid) submitButton.disabled = false;
else submitButton.disabled = true;
}
Form.addRemoveFunction(deleteButton, submissionForm, "account");

View File

@ -0,0 +1,70 @@
import * as Data from "../data.js";
import * as Form from "../form.js";
const submissionForm = document.getElementById('submission-form');
const sportDropdown = document.getElementById('sport-dropdown');
const genderDropdown = document.getElementById('gender-dropdown');
const nameTextbox = document.getElementById('name-textbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
const loadingSpan = document.getElementById('loading-message');
async function initializeForm() {
let params = new URLSearchParams(location.search);
let divisionID = params.get('division');
if(divisionID) {
const division = await (await fetch(`/fetch/manage/division?division=${divisionID}`)).json();
nameTextbox.value = division.name;
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
const gender = division.gender.name;
if(gender == 'female') genderDropdown.selectedIndex = 1;
else genderDropdown.selectedIndex = 2;
let data = {};
data.sports = [division.sport];
data.latestGame = {sportID : division.sportID };
Form.populateSports(sportDropdown, null, data);
Form.addHiddenValue('division', divisionID, submissionForm);
}
else {
Form.populateSports(sportDropdown);
genderDropdown.disabled = false;
sportDropdown.disabled = false;
}
nameTextbox.disabled = false;
nameTextbox.addEventListener('keyup', checkDataValidity);
loadingSpan.textContent = '';
submissionForm.style.visibility = 'visible';
}
initializeForm();
async function checkDataValidity() {
let dataIsValid = true;
if(!nameTextbox.value) dataIsValid = false;
const sportHasContent = sportDropdown.options.length;
const genderHasContent = genderDropdown.options.length;
if(!sportHasContent || !genderHasContent) dataIsValid = false;
if(dataIsValid) submitButton.disabled = false;
else submitButton.disabled = true;
}
Form.addRemoveFunction(deleteButton, submissionForm, "division");

View File

@ -0,0 +1,128 @@
import * as Data from "./../data.js";
import * as Form from "./../form.js";
const submissionForm = document.getElementById('submission-form');
const sportDropdown = document.getElementById('sport-dropdown');
const seasonDropdown = document.getElementById('year-dropdown');
const genderDropdown = document.getElementById('gender-dropdown');
const divisionDropdown = document.getElementById('division-dropdown');
const dateInput = document.getElementById('date-input');
const team1Dropdown = document.getElementById('team1-dropdown');
const team2Dropdown = document.getElementById('team2-dropdown');
const team1ScoreTextbox = document.getElementById('team1-score-textbox');
const team2ScoreTextbox = document.getElementById('team2-score-textbox');
const nameTextbox = document.getElementById('name-textbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
const loadingSpan = document.getElementById('loading-span');
async function initializeForm() {
let params = new URLSearchParams(location.search);
let gameID = params.get('game');
if(gameID) {
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
const game = await Data.getGame(gameID);
Form.addHiddenValue('game', gameID, submissionForm);
Form.populateSeasons(seasonDropdown, game.seasonID);
const data = await Data.getDivision(game.divisionID)
await Form.populateSports(sportDropdown, data.sportID)
await Form.populateGenders(genderDropdown, sportDropdown.value, data.gender.name)
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value, game.divisionID);
await Form.populateTeams(team1Dropdown, sportDropdown.value, game.team1ID);
await Form.populateTeams(team2Dropdown, sportDropdown.value, game.team2ID);
dateInput.value = game.date;
team1ScoreTextbox.value = game.team1Score;
team2ScoreTextbox.value = game.team2Score;
}
else {
/*try {*/
const data = await (await fetch(`/fetch/index/dropdown`)).json();
await Form.populateSeasons(seasonDropdown, null, data);
await Form.populateSports(sportDropdown, null, data);
await Form.populateGenders(genderDropdown, null, null, data);
await Form.populateDivisions(divisionDropdown, null, null, null, data);
await Form.populateTeams(team1Dropdown, null, null, data);
await Form.populateTeams(team2Dropdown, null, null, data, true);
/*} catch {
await Form.populateSeasons(seasonDropdown);
await Form.populateSports(sportDropdown)
await Form.populateGenders(genderDropdown, sportDropdown.value)
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
await Form.populateTeams(team1Dropdown, sportDropdown.value);
await Form.populateTeams(team2Dropdown, sportDropdown.value);
}*/
dateInput.value = (new Date()).toISOString().slice(0,10);
}
seasonDropdown.disabled = false;
sportDropdown.disabled = false;
genderDropdown.disabled = false;
divisionDropdown.disabled = false;
dateInput.disabled = false;
team1Dropdown.disabled = false;
team2Dropdown.disabled = false;
team1ScoreTextbox.disabled = false;
team2ScoreTextbox.disabled = false;
if(nameTextbox) {
nameTextbox.disabled = false;
}
sportDropdown.onchange = async () => {
await Form.populateGenders(genderDropdown, sportDropdown.value)
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
await Form.populateTeams(team1Dropdown, sportDropdown.value);
await Form.populateTeams(team2Dropdown, sportDropdown.value);
checkDataValidity();
};
genderDropdown.onchange = async () => {
await Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
checkDataValidity();
};
dateInput.addEventListener('keyup', checkDataValidity);
team1Dropdown.onchange = checkDataValidity;
team1ScoreTextbox.addEventListener('keyup', checkDataValidity);
team2Dropdown.onchange = checkDataValidity;
team2ScoreTextbox.addEventListener('keyup', checkDataValidity);
if(nameTextbox) nameTextbox.addEventListener('keyup', checkDataValidity);
loadingSpan.textContent = '';
submissionForm.style.visibility = 'visible';
checkDataValidity();
}
initializeForm();
async function checkDataValidity() {
let dataIsValid = true;
const seasonHasContent = seasonDropdown.options.length;
const sportHasContent = sportDropdown.options.length;
const genderHasContent = genderDropdown.options.length;
const divisionHasContent = divisionDropdown.options.length;
const team1HasContent = team1Dropdown.options.length;
const team2HasContent = team2Dropdown.options.length;
if(!seasonHasContent || !sportHasContent || !genderHasContent || !divisionHasContent || !team1HasContent || !team2HasContent) dataIsValid = false;
if(team1Dropdown.selectedIndex == team2Dropdown.selectedIndex) dataIsValid = false;
if(team1ScoreTextbox.value == "" || team2ScoreTextbox.value == "") dataIsValid = false;
if(dateInput.value == "") dataIsValid = false;
if(nameTextbox && nameTextbox.value == "") dataIsValid = false;
submitButton.disabled = !dataIsValid;
}
Form.addRemoveFunction(deleteButton, submissionForm, "game");

View File

@ -0,0 +1,132 @@
import * as Data from "./../data.js";
const gamesListTable = document.getElementById('games-list');
const addNewButton = document.getElementById('add-new-button');
const manageAccountButton = document.getElementById('manage-account-button');
function getGenderLetter(genderName) {
return genderName == "female" ? "F" : "M";
}
async function listGameHeaders() {
const headerRow = document.createElement('tr');
const teamsHeader = document.createElement('th');
teamsHeader.textContent = "Teams";
headerRow.appendChild(teamsHeader);
const scoreHeader = document.createElement('th');
headerRow.appendChild(scoreHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const sportNameHeader = document.createElement('th');
sportNameHeader.textContent = "Sport";
headerRow.appendChild(sportNameHeader);
const dateHeader = document.createElement('th');
dateHeader.textContent = "Date";
headerRow.appendChild(dateHeader);
gamesListTable.appendChild(headerRow);
}
function listGame(game, row) {
const teamsCell = document.createElement('td');
const team1NameSpan = document.createElement('span');
Data.getTeam(game.team1ID)
.then(data => team1NameSpan.textContent = data.name);
teamsCell.appendChild(team1NameSpan);
const team2NameSpan = document.createElement('span');
Data.getTeam(game.team2ID)
.then(data => team2NameSpan.textContent = data.name);
teamsCell.appendChild(team2NameSpan);
row.appendChild(teamsCell);
const scoresCell = document.createElement('td');
const team1ScoreSpan = document.createElement('span');
team1ScoreSpan.textContent = game.team1Score;
scoresCell.appendChild(team1ScoreSpan);
const team2ScoreSpan = document.createElement('span');
team2ScoreSpan.textContent = game.team2Score;
scoresCell.appendChild(team2ScoreSpan);
row.appendChild(scoresCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const sportCell = document.createElement('td');
const sportSpan = document.createElement('span');
const divisionSpan = document.createElement('span');
divisionSpan.classList.add('flat-content');
const divisionNameSpan = document.createElement('span');
const divisionGenderSpan = document.createElement('span');
divisionSpan.appendChild(divisionNameSpan);
divisionSpan.appendChild(divisionGenderSpan);
Data.getDivision(game.divisionID)
.then(data => {
Data.getSportName(data.sportID)
.then(data => sportSpan.textContent = data);
divisionNameSpan.textContent = data.name;
divisionGenderSpan.textContent = getGenderLetter(data.gender.name);
});
sportCell.appendChild(sportSpan);
sportCell.appendChild(divisionSpan);
row.appendChild(sportCell);
const dateCell = document.createElement('td');
const yearSpan = document.createElement('span');
yearSpan.textContent = game.date.slice(0,4);
dateCell.appendChild(yearSpan);
const dateSpan = document.createElement('span');
dateSpan.textContent = game.date.slice(5);
dateCell.appendChild(dateSpan);
row.appendChild(dateCell);
}
async function addGame() {
window.location.href = "/manage/game";
}
async function editGame(id) {
window.location.href = `/manage/game?game=${id}`;
}
async function listItems() {
const gamesList = await Data.getGamesByUser();
await listGameHeaders();
gamesList.forEach(game => {
const row = document.createElement('tr');
listGame(game, row);
const manageCell = document.createElement('td');
const editSpan = document.createElement('span');
const editButton = document.createElement('button');
editButton.textContent = "E";
editButton.addEventListener('click', () => {
editGame(game.id);
});
editSpan.appendChild(editButton);
manageCell.appendChild(editSpan);
row.appendChild(manageCell);
gamesListTable.appendChild(row);
});
}
listItems();
addNewButton.addEventListener('click', () => addGame());
manageAccountButton.addEventListener('click', () => {
window.location.href = '/manage/account';
});

View File

@ -0,0 +1,14 @@
const seasonTextbox = document.getElementById('season-textbox');
const submitButton = document.getElementById('submit-button');
async function checkDataValidity() {
let dataIsValid = true;
if(seasonTextbox.value == "") dataIsValid = false;
submitButton.disabled = !dataIsValid;
}
checkDataValidity();
seasonTextbox.addEventListener('keyup', checkDataValidity);
seasonTextbox.addEventListener('change', checkDataValidity);

View File

@ -0,0 +1,59 @@
import * as Data from "../data.js";
const nameTextbox = document.getElementById('name-textbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
const submissionForm = document.getElementById('submission-form');
const loadingSpan = document.getElementById('loading-message');
async function initializeForm() {
let params = new URLSearchParams(location.search);
let sportID = params.get('sport')
if(sportID) {
const sportName = await Data.getSportName(sportID);
nameTextbox.value = sportName;
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
const sportIDInput = document.createElement('input');
sportIDInput.setAttribute('name', 'sport');
sportIDInput.setAttribute('value', sportID);
sportIDInput.setAttribute('type', 'hidden');
submissionForm.appendChild(sportIDInput);
}
nameTextbox.disabled = false;
nameTextbox.addEventListener('keyup', checkDataValidity);
loadingSpan.textContent = '';
submissionForm.style.visibility = 'visible';
}
initializeForm();
async function checkDataValidity() {
let dataIsValid = true;
if(!nameTextbox.value) dataIsValid = false;
if(dataIsValid) submitButton.disabled = false;
else submitButton.disabled = true;
}
async function removeSport() {
const removeInput = document.createElement('input');
removeInput.setAttribute('name', 'remove');
removeInput.setAttribute('value', 1);
removeInput.setAttribute('type', 'hidden');
submissionForm.appendChild(removeInput);
submissionForm.submit();
}
deleteButton.addEventListener('click', () => {
const verified = confirm("This sport will be removed.");
if(verified) removeSport();
});

View File

@ -0,0 +1,57 @@
import * as Data from "../data.js";
import * as Form from "../form.js";
const submissionForm = document.getElementById('submission-form');
const sportDropdown = document.getElementById('sport-dropdown');
const nameTextbox = document.getElementById('name-textbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
const loadingSpan = document.getElementById('loading-message');
async function initializeForm() {
let params = new URLSearchParams(location.search);
let teamID = params.get('team');
if(teamID) {
const team = await (await fetch(`/fetch/manage/team?team=${teamID}`)).json();
nameTextbox.value = team.name;
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
let data = {};
data.sports = [team.sport];
data.latestGame = {sportID : team.sportID };
Form.populateSports(sportDropdown, null, data);
Form.addHiddenValue('team', teamID, submissionForm);
}
else {
sportDropdown.disabled = false;
Form.populateSports(sportDropdown);
}
nameTextbox.disabled = false;
nameTextbox.addEventListener('keyup', checkDataValidity);
loadingSpan.textContent = '';
submissionForm.style.visibility = 'visible';
}
initializeForm();
async function checkDataValidity() {
let dataIsValid = true;
if(!nameTextbox.value) dataIsValid = false;
const sportHasContent = sportDropdown.options.length;
if(!sportHasContent) dataIsValid = false;
submitButton.disabled = !dataIsValid;
}
Form.addRemoveFunction(deleteButton, submissionForm, "team");

View File

@ -0,0 +1,3 @@
#mobile-view {
max-width: 100%;
}

View File

@ -0,0 +1,55 @@
form {
display: flex;
flex-direction: column;
}
span {
display: flex;
flex-direction: column;
}
.form-main-dropdown {
width: 100%;
}
.form-section {
margin-bottom: 0.75em;
}
.form-section-input {
flex-direction: row;
}
input {
width: 100%;
}
.form-score-input{
width: 35%;
}
#submit-button {
margin-top: 1.5em;
}
#delete-button {
visibility: hidden;
}
.form-section-checkbox {
flex-direction: row;
align-items: center;
}
#admin-checkbox {
width: auto;
}
.flat-form-section {
flex-direction: row;
}
#submission-form {
visibility: hidden;
}

View File

@ -0,0 +1,38 @@
h1 {
text-align: left;
}
th {
text-align: left;
}
#score-column {
width: 20%;
}
#opponent-column{
width: 60%;
}
#date-column {
width: 20%;
}
tr {
height: 3em;
}
#header-div {
display: flex;
flex-direction: row;
}
#add-score-button {
margin-right: auto;
}
#login-button {
margin-left: auto;
}
#dropdowns-div {
visibility: hidden;
}

View File

@ -0,0 +1,11 @@
th {
text-align: left;
}
td {
white-space: nowrap;
}
.spacer-column {
width: 100%;
}

View File

@ -1,8 +1,71 @@
body { body {
padding: 50px; padding: 1em;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
} }
a { a {
color: #00B7FF; color: #00B7FF;
} }
#mobile-view {
max-width: 20em;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
}
.flat-content {
flex-direction: row;
justify-content: space-between;
}
.send-to-right {
margin-left: auto;
}
#actions-div {
display: flex;
}
#home-button {
margin-right: auto;
}
button {
padding: 0.25em;
margin: 0em 0.1em;
}
select {
padding: 0.25em;
}
input {
padding: 0.25em;
}
#logout-button {
margin-left: 0.5em;
}
#noscript-message {
background-color: lightcoral;
padding: 1em;
margin-bottom: 3em;
border-radius: .25em;
}
#about-footer {
margin-top: 3em;
text-align: center;
}
p {
line-height: 2;
}
a {
color: black;
}

View File

@ -0,0 +1,7 @@
h1 {
text-align: center;
}
#admin-checkbox-section {
visibility: hidden;
}

View File

@ -1,9 +1,8 @@
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) { router.get('/', function(req, res, next) {
res.send('respond with a resource'); res.render('about', { title: 'About Score Tracker', hideHomeButton: false });
}); });
module.exports = router; module.exports = router;

27
routes/auth.js 100644
View File

@ -0,0 +1,27 @@
var express = require('express');
var router = express.Router();
const passport = require('passport');
const app = require('../app');
router.get('/login', (req, res, next) => {
res.render('accounts/login', { title : "Login", message: req.flash('error') });
});
router.get('/logout', (req, res, next) => {
req.logout();
res.redirect("/");
});
router.post('/login',
passport.authenticate('local', {
failureRedirect: '/auth/login',
successRedirect: '/',
failureFlash: "Invalid email or password.",
}),
(req, res, next) => {
console.log(req.user);
});
module.exports = router;

View File

@ -0,0 +1,21 @@
function adminLoggedIn(req, res, next) {
if (req.user && req.user[2]) {
next();
}
else {
req.flash('error', 'An admin account is required to access this page.');
res.redirect('/auth/login');
}
}
function userLoggedIn(req, res, next) {
if (req.user) {
next();
}
else {
res.redirect('/auth/login');
}
}
exports.admin = adminLoggedIn;
exports.user = userLoggedIn;

167
routes/data.js 100644
View File

@ -0,0 +1,167 @@
var express = require('express');
var router = express.Router();
var sports = require('../database/scores/sports');
var seasons = require('../database/scores/seasons');
var genders = require('../database/scores/genders');
var divisions = require('../database/scores/divisions');
var teams = require('../database/scores/teams');
var games = require('../database/scores/games');
var accounts = require('../database/accounts/accounts');
var checkLoginStatus = require('./checkLoginStatus');
router.get('/sports', async function(req, res, next) {
try {
const data = await sports.retrieveAll();
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/sport', async function(req, res, next) {
try {
const sportID = req.query.sport;
const data = await sports.getFromID(sportID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/seasons', async function(req, res, next) {
try {
const data = await seasons.retrieveAll();
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/genders', async function(req, res, next) {
try {
const sportID = req.query.sport;
const data = await genders.retrieveBySport(sportID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/divisions', async function(req, res, next) {
try{
let gender;
if(req.query.gender) gender = (req.query.gender == 'female' ? genders.FEMALE : genders.MALE);
const sportID = req.query.sport;
const data = await divisions.retrieve(sportID, gender);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/division', async function(req, res, next) {
try {
const divisionID = req.query.division;
const data = await divisions.getFromID(divisionID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/teams', async function(req, res, next) {
try {
const sportID = req.query.sport;
const data = await teams.retrieve(sportID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/team', async function(req, res, next) {
try {
const teamID = req.query.team;
const data = await teams.getFromID(teamID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/games', async function(req, res, next) {
try {
const userID = req.user ? req.user[0] : null;
if(req.query.user) {
const data = await games.retrieveByUser(userID);
res.json(data);
} else {
const seasonID = req.query.season;
const divisionID = req.query.division;
const teamID = req.query.team;
const data = await games.retrieve(teamID, divisionID, seasonID);
res.json(data);
}
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/game', async function(req, res, next) {
try {
const gameID = req.query.game;
const ofUser = req.query.ofuser;
const currentUserID = req.user ? req.user[0] : null;
let data;
if(gameID) data = await games.getFromID(gameID);
else if(ofUser) data = await games.getLatest(currentUserID);
else data = await games.getLatest();
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/accounts', checkLoginStatus.admin, async function(req, res, next) {
try {
const data = await accounts.retrieveAll();
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
router.get('/account', checkLoginStatus.user, async function(req, res, next) {
try{
const userIsAdmin = req.user[2];
const loggedInAccountID = req.user[0];
const requestedAccountID = req.query.account;
if(!userIsAdmin && loggedInAccountID != requestedAccountID) {
res.status(403).send("ACCESS DENIED");
} else {
const data = await accounts.getFromID(req.query.account);
res.json(data);
}
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
})
module.exports = router;

200
routes/fetch.js 100644
View File

@ -0,0 +1,200 @@
var express = require('express');
var router = express.Router();
var sports = require('../database/scores/sports');
var seasons = require('../database/scores/seasons');
var genders = require('../database/scores/genders');
var divisions = require('../database/scores/divisions');
var teams = require('../database/scores/teams');
var games = require('../database/scores/games');
var accounts = require('../database/accounts/accounts');
var checkLoginStatus = require('./checkLoginStatus');
router.get('/index/dropdown', async function(req, res, next) {
let latestGame;
let seasonsData;
let sportsData;
let gendersData;
let divisionsData;
let teamsData;
try {
latestGame = await games.getLatest();
let division = await divisions.getFromID(latestGame.divisionID);
latestGame.sportID = division.sportID;
latestGame.gender = division.gender;
} catch {
latestGame = null;
}
try {
if(latestGame) {
seasonsData = await seasons.retrieveAll();
sportsData = await sports.retrieveAll();
gendersData = await genders.retrieveBySport(latestGame.sportID);
divisionsData = await divisions.retrieve(latestGame.sportID);
teamsData = await teams.retrieve(latestGame.sportID);
} else {
seasonsData = await seasons.retrieveAll();
sportsData = await sports.retrieveAll();
gendersData = await genders.retrieveBySport(sportsData[0].id);
divisionsData = await divisions.retrieve(sportsData[0].id);
teamsData = await teams.retrieve(sportsData[0].id);
}
res.json({
seasons : seasonsData,
sports : sportsData,
genders : gendersData,
divisions : divisionsData,
teams: teamsData,
latestGame
});
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/index/scores', async function (req, res, next) {
try {
const seasonID = req.query.season;
const divisionID = req.query.division;
const teamID = req.query.team;
const data = await games.retrieve(teamID, divisionID, seasonID);
for (const game of data) {
game.opponent = await teams.getFromID(game.team2ID);
}
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/submit', async function(req, res, next) {
let latestGame;
let seasonsData;
let sportsData;
let gendersData;
let divisionsData;
let teamsData;
const userID = req.user ? req.user[0] : null;
try {
latestGame = await games.getLatest(userID);
let division = await divisions.getFromID(latestGame.divisionID);
latestGame.sportID = division.sportID;
latestGame.gender = division.gender;
} catch {
latestGame = null;
}
try {
if(latestGame) {
seasonsData = await seasons.retrieveAll();
sportsData = await sports.retrieveAll();
gendersData = await genders.retrieveBySport(latestGame.sportID);
divisionsData = await divisions.retrieve(latestGame.sportID);
teamsData = await teams.retrieve(latestGame.sportID);
} else {
seasonsData = await seasons.retrieveAll();
sportsData = await sports.retrieveAll();
gendersData = await genders.retrieveBySport(sportsData[0].id);
divisionsData = await divisions.retrieve(sportsData[0].id);
teamsData = await teams.retrieve(sportsData[0].id);
}
res.json({
seasons : seasonsData,
sports : sportsData,
genders : gendersData,
divisions : divisionsData,
teams: teamsData,
latestGame
});
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/manage/divisions', async function (req, res, next) {
const data = await divisions.retrieve();
for(const division of data) {
division.sport = await sports.getFromID(division.sportID);
}
res.json(data);
});
router.get('/manage/division', async function (req, res, next) {
try {
const divisionID = req.query.division;
const data = await divisions.getFromID(divisionID);
data.sport = await sports.getFromID(data.sportID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/manage/teams', async function (req, res, next) {
const data = await teams.retrieve();
for(const team of data) {
team.sport = await sports.getFromID(team.sportID);
}
res.json(data);
});
router.get('/manage/team', async function (req, res, next) {
try {
const teamID = req.query.team;
const data = await teams.getFromID(teamID);
data.sport = await sports.getFromID(data.sportID);
res.json(data);
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
router.get('/manage/games', checkLoginStatus.user, async function (req, res, next) {
try{
const userIsAdmin = req.user[2];
const loggedInAccountID = req.user[0];
if(!userIsAdmin) {
res.status(403).send("ACCESS DENIED");
} else {
const data = await games.retrieve();
for(const game of data) {
game.team1 = await teams.getFromID(game.team1ID);
game.team2 = await teams.getFromID(game.team2ID);
game.division = await divisions.getFromID(game.divisionID);
game.sport = await sports.getFromID(game.division.sportID);
game.submitter = game.submitterName || (await accounts.getFromID(game.submitterID));
}
res.json(data);
}
} catch(err) {
console.error("ERROR: " + err.message);
res.status(500).send("An error has occurred");
}
});
module.exports = router;

View File

@ -1,9 +1,12 @@
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) { router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' }); res.render('index', { title: 'View Scores', userLoggedIn: !!req.user, hideHomeButton: true });
});
router.get('/submit', function(req, res, next) {
res.redirect('/manage/game');
}); });
module.exports = router; module.exports = router;

259
routes/manage.js 100644
View File

@ -0,0 +1,259 @@
var express = require('express');
var router = express.Router();
var genders = require('../database/scores/genders');
var games = require('../database/scores/games');
var seasons = require('../database/scores/seasons');
var sports = require('../database/scores/sports');
var divisions = require('../database/scores/divisions');
var genders = require('../database/scores/genders');
var teams = require('../database/scores/teams');
var accounts = require('../database/accounts/accounts');
var checkLoginStatus = require('./checkLoginStatus');
if (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV !== 'testing') {
require('dotenv').config();
}
router.get('/' ,checkLoginStatus.user, function(req, res, next) {
if(req.user[2]) res.render('manage', { title: 'Management Panel', userLoggedIn: !!req.user });
else res.render('manage/manage-nonadmin', { title: "My Games", userLoggedIn: !!req.user });
});
router.get('/game', function(req, res, next) {
if(!(process.env.PUBLIC_SUBMIT_PAGE && process.env.PUBLIC_SUBMIT_PAGE.toLowerCase() == 'true')) {
if (req.user) {
next();
}
else {
res.redirect('/auth/login');
}
} else {
next();
}
},
function(req, res, next) {
let title = req.query.game ? 'Edit Game' : 'Submit Score';
res.render('manage/addgame', { title, userLoggedIn: !!req.user, message: req.flash('error') });
});
router.post('/game', function(req, res, next) {
if(!(process.env.PUBLIC_SUBMIT_PAGE && process.env.PUBLIC_SUBMIT_PAGE.toLowerCase() == 'true')) {
if (req.user) {
next();
}
else {
res.redirect('/auth/login');
}
} else {
next();
}
},
async function(req, res, next) {
const id = req.body['game'];
const remove = req.body['remove'];
try {
const seasonID = req.body['year'];
const sportID = req.body['sport'];
const gender = (req.body['gender'] == "female") ? genders.FEMALE : genders.MALE;
const divisionID = req.body['division'];
const date = req.body['date'];
const team1ID = req.body['team1'];
const team1Score = req.body['team1-score'];
const team2ID = req.body['team2'];
const team2Score = req.body['team2-score'];
const submitterName = req.body['name'];
let submitterID;
let loggedInUserID;
let loggedInUserIsAdmin;
if(req.user) {
submitterID = req.user[0];
loggedInUserID = req.user[0];
loggedInUserIsAdmin = req.user[2];
}
const game = id ? await games.getFromID(id) : null;
if((!loggedInUserIsAdmin && game && loggedInUserID != game.submitterID) || (!req.user && game)) {
res.status(403).send("ACCESS DENIED");
}
else if(remove) {
await games.remove(id);
res.redirect('/manage#games');
}
else if(id) {
await games.edit(id, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score);
res.redirect('/manage#games');
}
else {
await games.add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, submitterID, submitterName);
res.redirect('/');
}
} catch(err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
res.redirect('/manage/game' + (id ? `?game=${id}` : ''));
}
});
router.get('/season', checkLoginStatus.admin, function(req, res, next) {
res.render('manage/addseason', { title: 'Add Season', currentYear : (new Date()).getFullYear(), userLoggedIn: !!req.user, message: req.flash('error') });
});
router.post('/season', checkLoginStatus.admin, async function(req, res, next) {
const seasonID = req.body['season'];
const remove = req.body['remove'];
try {
const year = req.body['year'];
if(remove) await seasons.remove(seasonID);
else await seasons.add(year);
res.redirect('/manage#seasons');
} catch(err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
res.redirect('/manage/season' + (seasonID ? `?season=${seasonID}` : ''));
}
});
router.get('/sport', checkLoginStatus.admin, function(req, res, next) {
let title = req.query.sport ? 'Edit Sport' : 'Add Sport';
res.render('manage/addsport', { title, userLoggedIn: !!req.user, message: req.flash('error') });
});
router.post('/sport', checkLoginStatus.admin, async function(req, res, next) {
const id = req.body['sport'];
const remove = req.body['remove'];
try {
const name = req.body['name'];
if(remove) await sports.remove(id);
else if(id) await sports.rename(id, name);
else await sports.add(name);
res.redirect('/manage#sports');
} catch(err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
res.redirect('/manage/sport' + (id ? `?sport=${id}` : ''));
}
});
router.get('/division', checkLoginStatus.admin, function(req, res, next) {
let title = req.query.division ? 'Edit Division' : 'Add Division'
res.render('manage/adddivision', { title, userLoggedIn: !!req.user, message: req.flash('error') });
});
router.post('/division', checkLoginStatus.admin, async function(req, res, next) {
const id = req.body['division'];
const remove = req.body['remove'];
try {
const name = req.body['name'];
const sport = req.body['sport'];
const genderName = req.body['gender'];
if(remove) await divisions.remove(id);
else if(id) await divisions.rename(id, name);
else {
if(genderName == 'both') {
await divisions.add(name, genders.FEMALE, sport);
await divisions.add(name, genders.MALE, sport);
} else {
const gender = (genderName == "female") ? genders.FEMALE : genders.MALE;
await divisions.add(name, gender, sport);
}
}
res.redirect('/manage#divisions');
} catch(err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
res.redirect('/manage/division' + (id ? `?division=${id}` : ''));
}
});
router.get('/team', checkLoginStatus.admin, function(req, res, next) {
let title = req.query.team ? 'Edit Team' : 'Add Team'
res.render('manage/addteam', { title, userLoggedIn: !!req.user, message: req.flash('error') });
});
router.post('/team', checkLoginStatus.admin, async function(req, res, next) {
const id = req.body['team'];
const remove = req.body['remove'];
try {
const name = req.body['name'];
const sport = req.body['sport'];
if(remove) teams.remove(id).then(res.redirect('/manage#teams'));
else if(id) teams.rename(id, name).then(res.redirect('/manage#teams'));
else teams.add(name, sport).then(res.redirect('/manage#teams'));
} catch(err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
res.redirect('/manage/team' + (id ? `?team=${id}` : ''));
}
});
router.get('/account', checkLoginStatus.user, (req, res, next) => {
const userIsAdmin = req.user[2];
const accountID = req.user[0];
if(userIsAdmin) {
let title = req.query.account ? 'Manage User' : 'Create User'
res.render('accounts/createuser', { title, userLoggedIn: !!req.user, message: req.flash('error') });
}
else {
let title = 'Manage Account';
res.render('accounts/createuser', { title, accountID, userLoggedIn: !!req.user, message: req.flash('error') });
}
});
router.post('/account', checkLoginStatus.user, async function(req, res, next) {
const name = req.body.name;
const email = req.body.email;
const password = req.body.password;
const accountID = req.body.account;
const remove = req.body.remove;
const loggedInAccountIsAdmin = req.user[2];
const loggedInAccountID = req.user[0];
if(!loggedInAccountIsAdmin && accountID != loggedInAccountID) {
res.status(403).send("ACCESS DENIED");
}
else {
try {
const isAdmin = loggedInAccountIsAdmin ? !!req.body.admin : false;
if(remove) await accounts.remove(accountID);
else if(accountID) await accounts.edit(accountID, email, password, isAdmin, name);
else await accounts.create(email, password, !!req.body.admin, name);
res.redirect('/manage#accounts');
}
catch (err) {
console.error("ERROR: " + err.message);
req.flash("error", "An error has occurred.");
let URL = '/manage/account';
if(loggedInAccountIsAdmin && accountID) URL += `?account=${accountID}`;
res.redirect(URL);
}
}
});
module.exports = router;

View File

@ -1,10 +0,0 @@
const request = require('supertest');
const app = require('../app');
describe('App', function() {
it('has the default page', function(done) {
request(app)
.get('/')
.expect(/Welcome to Express/, done);
});
});

13
views/about.pug 100644
View File

@ -0,0 +1,13 @@
extends layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/index.css')
block actions
block content
p Created by #[a(href="https://ethanreece.com") Ethan Reece], a student at #[a(href="https://colevalleychristian.org") Cole Valley Christian Schools].
p Need help? Found a bug? Email: #[a(href="mailto:scoretrackerhelp@ethanreece.com") scoretrackerhelp@ethanreece.com]
p #[a(href="https://gitlab.sudoer.ch/sudoer777/score-tracker") Git repo]

View File

@ -0,0 +1,35 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
span#loading-message Loading...
form#submission-form(action='/manage/account', method='POST')
if accountID
input#account-id(type="hidden" name="account" value=accountID)
span(class='form-section')
label Name
span(class='form-section-input')
input#name-textbox(type="text" name="name" disabled)
span(class='form-section')
label Email
span(class='form-section-input')
input#email-textbox(type="email" name="email" disabled)
span(class='form-section')
label Password
span(class='form-section-input' )
input#password-textbox(type="password" name="password" disabled)
span#admin-checkbox-section(class='form-section')
span(class='form-section-checkbox')
input#admin-checkbox(type="checkbox" name="admin" disabled)
label(for="admin-checkbox") Grant admin privileges
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(type="delete" disabled) Delete
block scripts
script(src='/scripts/manage/account.js' type="module")

View File

@ -0,0 +1,19 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
form(action='/auth/login', method='POST')
span(class='form-section')
label Email
span(class='form-section-input')
input(type="email", name="email")
span(class='form-section')
label Password
span(class='form-section-input')
input(type="password", name="password")
.error #{message}
span(class='form-section')
button#submit-button(type="submit") Submit

View File

@ -1,5 +1,8 @@
extends layout extends layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/error.css')
block content block content
h1= message h1= message
h2= error.status h2= error.status

View File

@ -1,5 +1,49 @@
extends layout extends layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/index.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block actions
if userLoggedIn
button#add-score-button Submit score
button#manage-button Manage...
block content block content
h1= title span#loading Loading...
p Welcome to #{title} div#dropdowns-div
span(class='form-section')
label Year
span(class='form-section-input')
select#year-dropdown(name="year" class="form-main-dropdown")
span(class='form-section flat-form-section')
span
label Sport
span(class='form-section-input')
select#sport-dropdown(name="sport" class="form-main-dropdown")
span
label Gender
span(class='form-section-input')
select#gender-dropdown(name="gender")
span
label Division
span(class='form-section-input')
select#division-dropdown(name="division")
span(class='form-section')
label Team
span(class='form-section-input')
select#team-dropdown(name="team" class="form-main-dropdown")
div
h2#games-table-header
span#no-scores-message
table
colgroup
col#score-column(span="1")
col#opponent-column(span="1")
col#date-column(span="1")
tbody#games-table
block scripts
script(src='/scripts/index.js' type="module")

View File

@ -1,7 +1,26 @@
doctype html doctype html
html html
head head
title= title title= title + ' - Score Tracker'
meta(name='viewport', content='width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1, user-scalable=no')
link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/stylesheets/style.css')
block stylesheets
body body
div#mobile-view
noscript
span#noscript-message Please enable JavaScript to run this app.
div#actions-div
if !hideHomeButton
button#home-button Home
block actions
if userLoggedIn
button#logout-button Log out
else if userLoggedIn !== undefined
button#login-button Log in
h1 #{title}
block content block content
div#about-footer
a(href="/about") Help/About
block scripts
script(src='/scripts/main.js' type="module")

26
views/manage.pug 100644
View File

@ -0,0 +1,26 @@
extends layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/manage.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
div
span(class='form-section')
label Category
span(class='form-section-input')
select#category-dropdown(name="category" class="form-main-dropdown")
option(value="seasons") Seasons
option(value="sports") Sports
option(value="divisions") Divisions
option(value="teams") Teams
option(value="games") Games
option(value="accounts") Accounts
div
h2#table-header
span#loading-message Loading...
table#items-list
button#add-new-button Add new...
block scripts
script(src='/scripts/manage.js' type="module")

View File

@ -0,0 +1,32 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
span#loading-message Loading...
form#submission-form(action='./division', method='POST')
span(class='form-section')
label Sport
span(class='form-section-input')
select#sport-dropdown(name="sport" class="form-main-dropdown" disabled)
span(class='form-section')
label Gender
span(class='form-section-input')
select#gender-dropdown(name="gender" class="form-main-dropdown" disabled)
option(value="both") Both
option(value="female") Female
option(value="male") Male
span(class='form-section')
label Division name
span(class='form-section-input')
input#name-textbox(type="text", name="name" disabled)
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(disabled) Delete
block scripts
script(src='/scripts/manage/division.js' type="module")

View File

@ -0,0 +1,61 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
span#loading-span Loading...
form#submission-form(action='./game', method='POST')
span(class='form-section')
label Year
span(class='form-section-input')
select#year-dropdown(name="year" class="form-main-dropdown" disabled)
span(class='form-section flat-form-section')
span
label Sport
span(class='form-section-input')
select#sport-dropdown(name="sport" class="form-main-dropdown")
span
label Gender
span(class='form-section-input')
select#gender-dropdown(name="gender")
span
label Division
span(class='form-section-input')
select#division-dropdown(name="division")
span(class='form-section')
label Date of match
span(class='form-section-input')
input#date-input(type="date", name="date" value=date disabled)
span(class='form-section flat-form-section')
span
label Your team
span(class='form-section-input')
select#team1-dropdown(name="team1" class="form-main-dropdown" disabled)
span(class='form-score-input')
label Score
span(class='form-section-input')
input#team1-score-textbox(type="number", name="team1-score", value="0" disabled)
span(class='form-section flat-form-section')
span
label Opponent
span(class='form-section-input')
select#team2-dropdown(name="team2" class="form-main-dropdown" disabled)
span(class='form-score-input')
label Score
span(class='form-section-input')
input#team2-score-textbox(type="number", name="team2-score", value="0" disabled)
if !userLoggedIn
span(class='form-section')
label Your name
span(class='form-section-input')
input#name-textbox(type="text" name="name" disabled)
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(disabled) Delete
block scripts
script(src='/scripts/manage/game.js' type="module")

View File

@ -0,0 +1,18 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
form(action='./season', method='POST')
span(class='form-section')
label Ending year
span(class='form-section-input')
input#season-textbox(type="number", name="year", value=currentYear)
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
block scripts
script(src='/scripts/manage/season.js' type="module")

View File

@ -0,0 +1,22 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
span#loading-message Loading...
form#submission-form(action='./sport', method='POST')
span(class='form-section')
label Sport name
span(class='form-section-input')
input#name-textbox(type="text" name="name" disabled)
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(disabled) Delete
block scripts
script(src='/scripts/manage/sport.js' type="module")

View File

@ -0,0 +1,25 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
span#loading-message Loading...
form#submission-form(action='./team', method='POST')
span(class='form-section')
label Sport
span(class='form-section-input')
select#sport-dropdown(name="sport" class="form-main-dropdown" disabled)
span(class='form-section')
label Team name
span(class='form-section-input')
input#name-textbox(type="text", name="name" disabled)
.error #{message}
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(disabled) Delete
block scripts
script(src='/scripts/manage/team.js' type="module")

View File

@ -0,0 +1,17 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/manage.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block actions
button#manage-account-button(class="send-to-right") Manage account
block content
div
table#games-list
div
button#add-new-button Add new...
block scripts
script(src='/scripts/manage/manage-nonadmin.js' type="module")