Browse Source

V2

new_auth
Dan 2 years ago
parent
commit
7aba67da2d
  1. 50
      .gitignore
  2. 10
      .vscode/launch.json
  3. 15
      backend/routes/upload-replay.js
  4. 78
      backend/trackmania-replays/gbx-header.js
  5. 377
      backend/trackmania-replays/gbx-replay.js
  6. 70
      backend/utilities/AdvancableBuffer.js
  7. 6
      backend/utilities/buffer-reader.js
  8. 4
      lerna.json
  9. 28556
      package-lock.json
  10. 67
      package.json
  11. 25
      packages/bridge-server/.eslintrc.js
  12. 35
      packages/bridge-server/.gitignore
  13. 4
      packages/bridge-server/.prettierrc
  14. 73
      packages/bridge-server/README.md
  15. 0
      packages/bridge-server/data/data.db
  16. 8
      packages/bridge-server/nest-cli.json
  17. 8843
      packages/bridge-server/package-lock.json
  18. 76
      packages/bridge-server/package.json
  19. 22
      packages/bridge-server/src/app.controller.spec.ts
  20. 12
      packages/bridge-server/src/app.controller.ts
  21. 33
      packages/bridge-server/src/app.module.ts
  22. 8
      packages/bridge-server/src/app.service.ts
  23. 18
      packages/bridge-server/src/auth/auth.controller.spec.ts
  24. 30
      packages/bridge-server/src/auth/auth.controller.ts
  25. 41
      packages/bridge-server/src/auth/auth.guard.ts
  26. 20
      packages/bridge-server/src/auth/auth.module.ts
  27. 18
      packages/bridge-server/src/auth/auth.service.spec.ts
  28. 24
      packages/bridge-server/src/auth/auth.service.ts
  29. 3
      packages/bridge-server/src/auth/constants.ts
  30. 19
      packages/bridge-server/src/auth/logger.middleware.ts
  31. 9
      packages/bridge-server/src/main.ts
  32. 18
      packages/bridge-server/src/seasons/seasons.controller.spec.ts
  33. 9
      packages/bridge-server/src/seasons/seasons.controller.ts
  34. 8
      packages/bridge-server/src/users/users.module.ts
  35. 18
      packages/bridge-server/src/users/users.service.spec.ts
  36. 24
      packages/bridge-server/src/users/users.service.ts
  37. 24
      packages/bridge-server/test/app.e2e-spec.ts
  38. 9
      packages/bridge-server/test/jest-e2e.json
  39. 4
      packages/bridge-server/tsconfig.build.json
  40. 21
      packages/bridge-server/tsconfig.json
  41. 12
      packages/bridge-shared/package.json
  42. 0
      packages/bridge-ui/.editorconfig
  43. 42
      packages/bridge-ui/.gitignore
  44. 0
      packages/bridge-ui/.vscode/extensions.json
  45. 20
      packages/bridge-ui/.vscode/launch.json
  46. 0
      packages/bridge-ui/.vscode/tasks.json
  47. 4
      packages/bridge-ui/README.md
  48. 66
      packages/bridge-ui/angular.json
  49. 42
      packages/bridge-ui/package.json
  50. 0
      packages/bridge-ui/src/app/app.component.html
  51. 0
      packages/bridge-ui/src/app/app.component.scss
  52. 16
      packages/bridge-ui/src/app/app.component.spec.ts
  53. 24
      packages/bridge-ui/src/app/app.component.ts
  54. 19
      packages/bridge-ui/src/app/app.config.ts
  55. 11
      packages/bridge-ui/src/app/app.routes.ts
  56. 1
      packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.html
  57. 0
      packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.scss
  58. 23
      packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.spec.ts
  59. 13
      packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.ts
  60. 18
      packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.html
  61. 12
      packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.scss
  62. 23
      packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.spec.ts
  63. 60
      packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts
  64. 39
      packages/bridge-ui/src/app/components/race-details/race-details.component.html
  65. 0
      packages/bridge-ui/src/app/components/race-details/race-details.component.scss
  66. 10
      packages/bridge-ui/src/app/components/race-details/race-details.component.spec.ts
  67. 106
      packages/bridge-ui/src/app/components/race-details/race-details.component.ts
  68. 10
      packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.html
  69. 32
      packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.scss
  70. 23
      packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.spec.ts
  71. 23
      packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.ts
  72. 13
      packages/bridge-ui/src/app/components/season-card/season-card.component.html
  73. 26
      packages/bridge-ui/src/app/components/season-card/season-card.component.scss
  74. 10
      packages/bridge-ui/src/app/components/season-card/season-card.component.spec.ts
  75. 33
      packages/bridge-ui/src/app/components/season-card/season-card.component.ts
  76. 23
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.html
  77. 0
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.scss
  78. 23
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.spec.ts
  79. 155
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts
  80. 40
      packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.html
  81. 0
      packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.scss
  82. 23
      packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.spec.ts
  83. 33
      packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.ts
  84. 25
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.html
  85. 4
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.scss
  86. 10
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.spec.ts
  87. 66
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts
  88. 3
      packages/bridge-ui/src/app/interceptors/auth.interceptor.ts
  89. 10
      packages/bridge-ui/src/app/models/race.model.ts
  90. 7
      packages/bridge-ui/src/app/models/raceEntry.model.ts
  91. 5
      packages/bridge-ui/src/app/models/racer.model.ts
  92. 9
      packages/bridge-ui/src/app/models/season.model.ts
  93. 7
      packages/bridge-ui/src/app/models/theme-option.model.ts
  94. 23
      packages/bridge-ui/src/app/pages/getting-started/getting-started.component.html
  95. 18
      packages/bridge-ui/src/app/pages/getting-started/getting-started.component.scss
  96. 23
      packages/bridge-ui/src/app/pages/getting-started/getting-started.component.spec.ts
  97. 22
      packages/bridge-ui/src/app/pages/getting-started/getting-started.component.ts
  98. 1
      packages/bridge-ui/src/app/pages/home/home.component.html
  99. 0
      packages/bridge-ui/src/app/pages/home/home.component.scss
  100. 23
      packages/bridge-ui/src/app/pages/home/home.component.spec.ts

50
.gitignore

@ -1,49 +1 @@
# See http://help.github.com/ignore-files/ for more about ignoring files. node_modules/
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
# Uploads
/uploads
nedb-database/raceresults.db
nedb-database/racers.db
nedb-database/races.db
nedb-database/seasons.db

10
.vscode/launch.json

@ -1,20 +1,16 @@
{ {
// 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 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"cwd": "${workspaceFolder}/pakcages/bridge-ui/",
"name": "ng serve", "name": "ng serve",
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
"preLaunchTask": "npm: start", "preLaunchTask": "npm: start",
"url": "http://localhost:4200/" "url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
} }
] ]
} }

15
backend/routes/upload-replay.js

@ -1,15 +0,0 @@
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
}
});
// Create the multer instance
const upload = multer({ storage: storage});
module.exports = upload;

78
backend/trackmania-replays/gbx-header.js

@ -1,78 +0,0 @@
class gbxHeader {
has_magic = true
version = -1
format = -1
compressionofRefTable = -1
compressionOfrefBofy = -1
compressionTextFlag = ''
id = -1
userData = []
numNodes = 0
is_vaild = false
parse(buff)
{
let header_magic = buff.readString(3);
if (header_magic != 'GBX')
{
console.log("Header Magic Mismatch: " + header_magic);
return
}
this.has_magic = true
this.version = buff.readUInt16();
console.log(this.version)
if (this.version < 5 || this.version >7 )
{
console.log("Unsupported version")
return;
}
this.format = buff.readInt8();
if (this.format != 66)
{
console.log("Unsupported format")
return;
}
this.compressionofRefTable = buff.readInt8();
if (this.compressionofRefTable != 67 && this.compressionofRefTable != 85)
{
console.log("Unsupported compression format, Ref")
return;
}
this.compressionofRefBody = buff.readInt8();
if (this.compressionofRefBody != 67 && this.compressionofRefBody != 85)
{
console.log("Unsupported compression format, Body")
return;
}
this.compressionTextFlag = '';
if (this.version >= 4)
{
this.compressionTextFlag = buff.readString(1);
}
this.id = buff.readUInt32();
console.log(this.id)
if (this.version >=6)
{
this.userData = buff.readBytes();
}
this.numNodes = buff.readUInt32();
this.is_vaild = true
}
}
module.exports = gbxHeader;

377
backend/trackmania-replays/gbx-replay.js

@ -1,377 +0,0 @@
class gbxReplay {
CHUNKS_OFFSET = 17;
HEADER_OFFSET = 21;
UNASSIGNED = 0xFFFFFFFF;
mapUID = "";
environment = "";
author = "";
bestTime = this.UNASSIGNED;
gamerHandle = ""
login = ""
titleId = ""
calcChunkOffset(num) {
return (((num) * 8) + this.HEADER_OFFSET)
}
isNumber(id) { return (((id) & 0xC0000000) == 0)}
isString(id) { return (((id) & 0xC0000000) != 0)}
isUnassigned(id) {return ((id) == this.UNASSIGNED)}
getIndex(id) {return ((id) & 0x3FFFFFFF)}
parseReplayChunk(buff, chunkInfo) {
buff.seek(0+chunkInfo.dwOffset);
let version = buff.readUInt32();
let isVSK = version >= 9999 ? true : false;
let idList = { version: 0, index: 0, list:[]}
if( chunkInfo.dwSize <= 4) {
return;
}
if((!isVSK && version >= 3) || (isVSK && version >= 10000))
{
let result = this.readIdentifier(buff, idList);
if(result.read > 0)
{
this.mapUID = result.str;
console.log("MapUID:\t " + this.mapUID)
}
result = this.readIdentifier(buff, idList);
if(result.read > 0)
{
this.environment = result.str;
console.log("Environ:\t " + this.environment)
}
result = this.readIdentifier(buff, idList)
if (result.read > 0)
{
this.author = result.str;
console.log("Author:\t " + this.author)
}
this.bestTime = buff.readUInt32();
console.log("BestTime:\t " + this.bestTime);
result = buff.readNadeoString();
if (result.read > 0)
{
this.gamerHandle = result.str;
console.log("Gamer Handle:\t " + this.gamerHandle)
}
if((!isVSK && version >= 6) || (isVSK && version >= 10000))
{
result = buff.readNadeoString();
if (result.read > 0)
{
this.login = result.str;
console.log("Login:\t " + this.login)
}
if((!isVSK && version >= 8) || (isVSK && version >= 10000))
{
buff.seek(buff.readPos+1);
result = this.readIdentifier(buff, idList)
if (result.read > 0)
{
this.titleId = result.str;
console.log("titleId:\t " + this.titleId)
}
}
}
}
}
parseAuthorChunk(buff, chunkInfo) {
}
parseCommunityChunk(buff, chunkInfo) {
}
parse(buff)
{
buff.seek(0+this.CHUNKS_OFFSET);
let numChunks = buff.readUInt32();
if(numChunks == 0) {
return true;
}
else if (numChunks > 0xff) {
return false;
}
let chunkId, chunkSize = 0;
let chunkOffset = this.calcChunkOffset(numChunks)
let chunkVersion = {}
let chunkCommunity = {}
let chunkAuthor = {}
for (let counter = 1; counter <= numChunks; counter++) {
chunkId = buff.readUInt32();
chunkSize = buff.readUInt32();
chunkSize &= 0x7FFFFFFF;
switch (chunkId){
case 0x03093000: // (TM)
case 0x2403F000: // (VSK, TM)
chunkVersion.dwId = chunkId;
chunkVersion.dwSize = chunkSize;
chunkVersion.dwOffset = chunkOffset;
chunkOffset += chunkSize;
break;
case 0x03093001: // (TM)
case 0x2403F001: // (VSK, TM)
chunkCommunity.dwId = chunkId;
chunkCommunity.dwSize = chunkSize;
chunkCommunity.dwOffset = chunkOffset;
chunkOffset += chunkSize;
//OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize);
break;
case 0x03093002: // (MP)
chunkAuthor.dwId = chunkId;
chunkAuthor.dwSize = chunkSize;
chunkAuthor.dwOffset = chunkOffset;
chunkOffset += chunkSize;
//OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize);
break;
default:
chunkOffset += chunkSize;
//OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize);
}
}
if (chunkVersion.dwSize > 0) {
this.parseReplayChunk(buff, chunkVersion);
}
if (chunkCommunity.dwSize > 0) {
this.parseCommunityChunk(buff, chunkCommunity);
}
if (chunkAuthor.dwSize > 0) {
this.parseAuthorChunk(buff, chunkAuthor);
}
}
readIdentifier(buff, idList)
{
if ( idList.version < 3 )
{
idList.version = buff.readUInt32();
if (idList.version < 2)
{
return {read:-1, str:""};
}
}
let id = buff.readUInt32();
if(this.isUnassigned(id))
{
return {read:0, str:""};
}
if(this.isNumber(id))
{
return this.getCollectionString(id);
}
if(idList.version == 2)
{
return buff.readNadeoString();
}
if(this.isString(id) && this.getIndex(id) == 0)
{
let result = buff.readNadeoString();
if (result.read > 0 && idList.index < 8)
{
idList.list[idList.index] = result.str;
idList.index++;
}
return result;
}
let index = this.getIndex(id);
if( index == 0 || index > 8)
{
return {read:0, str:""};
}
let latest = idList.list[idList.index-1];
return {read:latest.length, str:latest};
}
getCollectionString(id)
{
let outString = ""
switch (id)
{
case 0: // Speed
outString = "Desert";
break;
case 1: // Alpine
outString = "Snow";
break;
case 2: // Rally
outString = "Rally";
break;
case 3: // Island
outString = "Island";
break;
case 4: // Bay
outString = "Bay";
break;
case 5: // Coast
outString = "Coast";
break;
case 6: // StadiumMP4
outString = "Stadium";
break;
case 7: // Basic
outString = "Basic";
break;
case 8: // Plain
outString = "Plain";
break;
case 9: // Moon
outString = "Moon";
break;
case 10: // Toy
outString = "Toy";
break;
case 11: // Valley
outString = "Valley";
break;
case 12: // Canyon
outString = "Canyon";
break;
case 13: // Lagoon
outString = "Lagoon";
break;
case 14: // Deprecated_Arena
outString = "Arena";
break;
case 15: // TMTest8
outString = "TMTest8";
break;
case 16: // TMTest9
outString = "TMTest9";
break;
case 17: // TMCommon
outString = "TMCommon";
break;
case 18: // Canyon4
outString = "Canyon4";
break;
case 19: // Canyon256
outString = "Canyon256";
break;
case 20: // Valley4
outString = "Valley4";
break;
case 21: // Valley256
outString = "Valley256";
break;
case 22: // Lagoon4
outString = "Lagoon4";
break;
case 23: // Lagoon256
outString = "Lagoon256";
break;
case 24: // Stadium4
outString = "Stadium4";
break;
case 25: // Stadium256
outString = "Stadium256";
break;
case 26: // Stadium
outString = "Stadium";
break;
case 27: // Voxel
outString = "Voxel";
break;
case 100: // History
outString = "History";
break;
case 101: // Society
outString = "Society";
break;
case 102: // Galaxy
outString = "Galaxy";
break;
case 103: // QMTest1
outString = "QMTest1";
break;
case 104: // QMTest2
outString = "QMTest2";
break;
case 105: // QMTest3
outString = "QMTest3";
break;
case 200: // Gothic
outString = "Gothic";
break;
case 201: // Paris
outString = "Paris";
break;
case 202: // Storm
outString = "Storm";
break;
case 203: // Cryo
outString = "Cryo";
break;
case 204: // Meteor
outString = "Meteor";
break;
case 205: // Meteor4
outString = "Meteor4";
break;
case 206: // Meteor256
outString = "Meteor256";
break;
case 207: // SMTest3
outString = "SMTest3";
break;
case 299: // SMCommon
outString = "SMCommon";
break;
case 10000: // Vehicles
outString = "Vehicles";
break;
case 10001: // Orbital
outString = "Orbital";
break;
case 10002: // Actors
outString = "Actors";
break;
case 10003: // Common
outString = "Common";
break;
case UNASSIGNED:
outString = "_Unassigned";
break;
default:
{
return {read:0, str:""};
}
}
return {read:outString.length, str:outString}
}
}
module.exports = gbxReplay;

70
backend/utilities/AdvancableBuffer.js

@ -1,70 +0,0 @@
class Advancablebuffer {
readPos = 0;
internalBuffer;
littleEndian = true
constructor(buffer, useBigEndian)
{
this.internalBuffer = buffer;
this.readPos = 0;
this.littleEndian = !useBigEndian;
}
seek(seekPos)
{
this.readPos = seekPos
}
readString(numBytes)
{
let t = this.internalBuffer.subarray(this.readPos, this.readPos+=numBytes);
return t.toString();
}
readInt8()
{
let value = this.internalBuffer.readInt8(this.readPos);
this.readPos += 1;
return value;
}
readUInt16()
{
let value = this.littleEndian ? this.internalBuffer.readUInt16LE(this.readPos) : this.internalBuffer.readUInt16BE(this.readPos);
this.readPos += 2;
return value;
}
readUInt32()
{
let value = this.littleEndian ? this.internalBuffer.readUInt32LE(this.readPos) : this.internalBuffer.readUInt32BE(this.readPos);
this.readPos += 4;
return value;
}
readBytes()
{
let length = this.readUInt32()
let sub = this.internalBuffer.subarray(this.readPos, this.readPos+=length);
return sub;
}
readNadeoString()
{
let length = this.readUInt32()
if(length >= 0xFFFF)
{
return {read:-1, str:""};
}
if (length == 0)
{
return {read:0, str:""};
}
let outString = this.readString(length);
return {read:outString.length, str:outString};
}
}
module.exports = Advancablebuffer;

6
backend/utilities/buffer-reader.js

@ -1,6 +0,0 @@
function readStringAndAdvance(buffer, start, numBytes)
{
let pos = start
let string = buffer.subarray(pos, pos+=numBytes);
return string.toString(), pos;
}

4
lerna.json

@ -0,0 +1,4 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.0.0"
}

28556
package-lock.json

File diff suppressed because it is too large

67
package.json

@ -1,59 +1,20 @@
{ {
"name": "bridge", "name": "bridge",
"version": "0.0.0", "version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": { "scripts": {
"ng": "ng", "test": "echo \"Error: no test specified\" && exit 1"
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"start:server": "nodemon server.js"
},
"private": true,
"dependencies": {
"@angular/animations": "^16.2.0",
"@angular/cdk": "^16.2.11",
"@angular/common": "^16.2.0",
"@angular/compiler": "^16.2.0",
"@angular/core": "^16.2.0",
"@angular/forms": "^16.2.0",
"@angular/material": "^16.2.11",
"@angular/platform-browser": "^16.2.0",
"@angular/platform-browser-dynamic": "^16.2.0",
"@angular/router": "^16.2.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"@types/jwt-decode": "^3.1.0",
"axios": "^1.6.2",
"better-sqlite3": "^9.1.1",
"camo": "^0.12.4",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"mdb-angular-ui-kit": "^5.1.0",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"nedb": "^1.8.0",
"node-sass": "^9.0.0",
"nodemon": "^3.0.1",
"rxjs": "~7.8.0",
"sass-loader": "^13.3.2",
"trackmania.io": "^3.2.2",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"
}, },
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^16.2.9", "lerna": "^7.4.2"
"@angular/cli": "^16.2.9", },
"@angular/compiler-cli": "^16.2.0", "workspaces": [
"@types/jasmine": "~4.3.0", "packages/bridge-server",
"jasmine-core": "~4.6.0", "packages/bridge-ui",
"karma": "~6.4.0", "packages/bridge-shared"
"karma-chrome-launcher": "~3.2.0", ]
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.1.3"
}
} }

25
packages/bridge-server/.eslintrc.js

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

35
packages/bridge-server/.gitignore

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

4
packages/bridge-server/.prettierrc

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

73
packages/bridge-server/README.md

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

0
src/app/components/admin/admin.component.scss → packages/bridge-server/data/data.db

8
packages/bridge-server/nest-cli.json

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

8843
packages/bridge-server/package-lock.json

File diff suppressed because it is too large

76
packages/bridge-server/package.json

@ -0,0 +1,76 @@
{
"name": "bridge-server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/sequelize": "^10.0.0",
"bridge-shared": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"sequelize": "^6.35.1",
"sequelize-typescript": "^2.1.5",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/sequelize": "^4.28.18",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

22
packages/bridge-server/src/app.controller.spec.ts

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
packages/bridge-server/src/app.controller.ts

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

33
packages/bridge-server/src/app.module.ts

@ -0,0 +1,33 @@
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SequelizeModule } from '@nestjs/sequelize';
import { AuthModule } from './auth/auth.module';
import { UsersService } from './users/users.service';
import { UsersModule } from './users/users.module';
import { LoggerMiddleware } from './auth/logger.middleware';
import { SeasonsController } from './seasons/seasons.controller';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'sqlite',
storage: 'data/data.db',
autoLoadModels: true,
synchronize: true,
}),
AuthModule,
UsersModule,
],
controllers: [AppController, SeasonsController],
providers: [AppService, UsersService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}

8
packages/bridge-server/src/app.service.ts

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

18
packages/bridge-server/src/auth/auth.controller.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

30
packages/bridge-server/src/auth/auth.controller.ts

@ -0,0 +1,30 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
console.log(req.user);
return req.user;
}
}

41
packages/bridge-server/src/auth/auth.guard.ts

@ -0,0 +1,41 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

20
packages/bridge-server/src/auth/auth.module.ts

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '1h' },
}),
],
controllers: [AuthController],
providers: [AuthService]
})
export class AuthModule {}

18
packages/bridge-server/src/auth/auth.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

24
packages/bridge-server/src/auth/auth.service.ts

@ -0,0 +1,24 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async signIn(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.userId, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}

3
packages/bridge-server/src/auth/constants.ts

@ -0,0 +1,3 @@
export const jwtConstants = {
secret: 'ThisLittlePiggy',
};

19
packages/bridge-server/src/auth/logger.middleware.ts

@ -0,0 +1,19 @@
import { Injectable, NestMiddleware, RequestMethod } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { RouteInfo } from '@nestjs/common/interfaces';
import { request } from 'http';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Gets the request log
console.log(`req:`, {
headers: req.headers,
body: req.body,
originalUrl: req.originalUrl,
});
// Ends middleware function execution, hence allowing to move on
if (next) {
next();
}
}
}

9
packages/bridge-server/src/main.ts

@ -0,0 +1,9 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
bootstrap();

18
packages/bridge-server/src/seasons/seasons.controller.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SeasonsController } from './seasons.controller';
describe('SeasonsController', () => {
let controller: SeasonsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SeasonsController],
}).compile();
controller = module.get<SeasonsController>(SeasonsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

9
packages/bridge-server/src/seasons/seasons.controller.ts

@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('seasons')
export class SeasonsController {
@Get()
findAll(): string {
return ''
}
}

8
packages/bridge-server/src/users/users.module.ts

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

18
packages/bridge-server/src/users/users.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

24
packages/bridge-server/src/users/users.service.ts

@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
@Injectable()
export class UsersService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
},
];
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}

24
packages/bridge-server/test/app.e2e-spec.ts

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
packages/bridge-server/test/jest-e2e.json

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
packages/bridge-server/tsconfig.build.json

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
packages/bridge-server/tsconfig.json

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

12
packages/bridge-shared/package.json

@ -0,0 +1,12 @@
{
"name": "bridge-shared",
"version": "1.0.0",
"description": "",
"main": "index.js",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

0
.editorconfig → packages/bridge-ui/.editorconfig

42
packages/bridge-ui/.gitignore

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

0
.vscode/extensions.json → packages/bridge-ui/.vscode/extensions.json

20
packages/bridge-ui/.vscode/launch.json

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

0
.vscode/tasks.json → packages/bridge-ui/.vscode/tasks.json

4
README.md → packages/bridge-ui/README.md

@ -1,6 +1,6 @@
# Bridge # BridgeUi
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.9. This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.
## Development server ## Development server

66
angular.json → packages/bridge-ui/angular.json

@ -3,7 +3,7 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"bridge": { "bridge-ui": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -15,11 +15,11 @@
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "dist/bridge", "outputPath": "dist/bridge-ui",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": [
"zone.js" "zone.js"
], ],
@ -30,7 +30,27 @@
"src/assets" "src/assets"
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/indigo-pink.css", {
"input": "src/assets/indigo-pink.css",
"inject": false,
"bundleName": "indigo-pink"
},
{
"input": "src/assets/deeppurple-amber.css",
"inject": false,
"bundleName": "deeppurple-amber"
},
{
"input": "src/assets/pink-bluegrey.css",
"inject": false,
"bundleName": "pink-bluegrey"
},
{
"input": "src/assets/purple-green.css",
"inject": false,
"bundleName": "purple-green"
},
"src/assets/purple-green.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []
@ -52,12 +72,9 @@
"outputHashing": "all" "outputHashing": "all"
}, },
"development": { "development": {
"buildOptimizer": false,
"optimization": false, "optimization": false,
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true
"namedChunks": true
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@ -66,10 +83,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "bridge:build:production" "buildTarget": "bridge-ui:build:production"
}, },
"development": { "development": {
"browserTarget": "bridge:build:development" "buildTarget": "bridge-ui:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@ -77,7 +94,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "bridge:build" "buildTarget": "bridge-ui:build"
} }
}, },
"test": { "test": {
@ -94,7 +111,27 @@
"src/assets" "src/assets"
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/indigo-pink.css", {
"input": "@angular/material/prebuilt-themes/indigo-pink.css",
"inject": false,
"bundleName": "indigo-pink"
},
{
"input": "@angular/material/prebuilt-themes/deeppurple-amber.css",
"inject": false,
"bundleName": "deeppurple-amber"
},
{
"input": "@angular/material/prebuilt-themes/pink-bluegrey.css",
"inject": false,
"bundleName": "pink-bluegrey"
},
{
"input": "@angular/material/prebuilt-themes/purple-green.css",
"inject": false,
"bundleName": "purple-green"
},
"@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []
@ -102,5 +139,8 @@
} }
} }
} }
},
"cli": {
"analytics": "955048a1-2b3d-4c4f-8a58-40798c5b94d1"
} }
} }

42
packages/bridge-ui/package.json

@ -0,0 +1,42 @@
{
"name": "bridge-ui",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/cdk": "^17.0.1",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/material": "^17.0.1",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"bridge-shared": "^1.0.0",
"jwt-decode": "^4.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.1",
"@angular/cli": "^17.0.1",
"@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2"
}
}

0
src/app/app.component.html → packages/bridge-ui/src/app/app.component.html

0
src/app/components/login/login.component.scss → packages/bridge-ui/src/app/app.component.scss

16
src/app/app.component.spec.ts → packages/bridge-ui/src/app/app.component.spec.ts

@ -1,12 +1,12 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(() => TestBed.configureTestingModule({ beforeEach(async () => {
imports: [RouterTestingModule], await TestBed.configureTestingModule({
declarations: [AppComponent] imports: [AppComponent],
})); }).compileComponents();
});
it('should create the app', () => { it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
@ -14,16 +14,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy(); expect(app).toBeTruthy();
}); });
it(`should have as title 'bridge'`, () => { it(`should have the 'bridge-ui' title`, () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app.title).toEqual('bridge'); expect(app.title).toEqual('bridge-ui');
}); });
it('should render title', () => { it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('bridge app is running!'); expect(compiled.querySelector('h1')?.textContent).toContain('Hello, bridge-ui');
}); });
}); });

24
packages/bridge-ui/src/app/app.component.ts

@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { TopBarComponent } from './components/top-bar/top-bar.component';
import { ThemeService } from './services/theme.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, TopBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'bridge-ui';
defaultTheme = 'purple-green'
constructor (private themeService: ThemeService) {}
ngOnInit() {
this.themeService.setTheme(this.defaultTheme);
}
}

19
packages/bridge-ui/src/app/app.config.ts

@ -0,0 +1,19 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations(),
provideHttpClient(
withInterceptorsFromDi()
),
{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
]
};

11
packages/bridge-ui/src/app/app.routes.ts

@ -0,0 +1,11 @@
import { Routes } from '@angular/router';
import { SeasonsComponent } from './pages/seasons/seasons.component';
import { SeasonDetailsComponent } from './pages/season-details/season-details.component';
import { GettingStartedComponent } from './pages/getting-started/getting-started.component';
export const routes: Routes = [
{ path: '', redirectTo: '/seasons', pathMatch:'full' },
{ path: 'seasons', component: SeasonsComponent },
{ path: 'seasons/:id', component: SeasonDetailsComponent },
{ path: 'getting-started', component: GettingStartedComponent },
];

1
packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.html

@ -0,0 +1 @@
<p>bottom-bar works!</p>

0
src/app/components/new-race-dialog/new-race-dialog.component.scss → packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.scss

23
packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BottomBarComponent } from './bottom-bar.component';
describe('BottomBarComponent', () => {
let component: BottomBarComponent;
let fixture: ComponentFixture<BottomBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BottomBarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(BottomBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

13
packages/bridge-ui/src/app/components/bottom-bar/bottom-bar.component.ts

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-bottom-bar',
standalone: true,
imports: [CommonModule],
templateUrl: './bottom-bar.component.html',
styleUrl: './bottom-bar.component.scss'
})
export class BottomBarComponent {
}

18
packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.html

@ -0,0 +1,18 @@
<div class="signInForm">
<h1 mat-dialog-title>Login</h1>
<div mat-dialog-content>
<mat-form-field class="full-width">
<mat-label>Username</mat-label>
<input matInput type="text" id="username" [formControl]="username" name="username" required>
</mat-form-field>
<br/>
<mat-form-field class="full-width">
<mat-label>Password</mat-label>
<input matInput type="password" id="password" [formControl]="password" name="password" required>
</mat-form-field>
</div>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-dialog-close (click)="login()">Login</button>
</mat-dialog-actions>
</div>

12
packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.scss

@ -0,0 +1,12 @@
.signInForm {
width: 400px;
padding: 12px 24px 24px;
igx-input-group + igx-input-group {
margin-top: 24px;
}
}
.full-width {
width: 100%;
}

23
packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginDialogComponent } from './login-dialog.component';
describe('LoginDialogComponent', () => {
let component: LoginDialogComponent;
let fixture: ComponentFixture<LoginDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginDialogComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

60
packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts

@ -0,0 +1,60 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import {
MatDialog,
MAT_DIALOG_DATA,
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose,
} from '@angular/material/dialog';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatInputModule,
MatFormFieldModule,
FormsModule,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose,
],
templateUrl: './login-dialog.component.html',
styleUrl: './login-dialog.component.scss'
})
export class LoginDialogComponent {
username = new FormControl('');
password = new FormControl('');
constructor(private authService: AuthService) {}
login(): void {
let username = this.username.value != undefined ? this.username.value : "";
let password = this.password.value != undefined ? this.password.value : "";
this.authService.login(username, password).subscribe(data => {
console.log(data)
if (data.access_token) {
localStorage.setItem('token', data.access_token);
window.location.reload();
}
})
}
}

39
packages/bridge-ui/src/app/components/race-details/race-details.component.html

@ -0,0 +1,39 @@
@if( race != undefined ) {
<mat-grid-list cols="4" rowHeight="100px">
<mat-grid-tile [colspan]="3" [rowspan]="1"><h1>{{race.mapName}}</h1></mat-grid-tile>
<mat-grid-tile [colspan]="1" [rowspan]="2"><img
src={{race.mapImgUrl}}
class="img-thumbnail shadow-2-strong"
/></mat-grid-tile>
<mat-grid-tile [colspan]="3" [rowspan]="1">Three</mat-grid-tile>
</mat-grid-list>
<br/>
<div>
<table mat-table [dataSource]="sortedResults" class="mat-elevation-z8">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element; let i = index">{{i + 1}}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Name </th>
<td mat-cell *matCellDef="let element"> {{getRacerName(element.racer_id)}} </td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="runTime">
<th mat-header-cell *matHeaderCellDef>Time </th>
<td mat-cell *matCellDef="let element"> {{formatMilliseconds(element.time)}} </td>
</ng-container>
<ng-container matColumnDef="ghost">
<th mat-header-cell *matHeaderCellDef>Ghost</th>
<td mat-cell *matCellDef="let element"> <button mat-raised-button color="accent"><mat-icon aria-hidden="false" aria-label="Example home icon" fontIcon="download"></mat-icon> Ghost</button> </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}

0
src/app/components/race-details/race-details.component.scss → packages/bridge-ui/src/app/components/race-details/race-details.component.scss

10
src/app/components/race-details/race-details.component.spec.ts → packages/bridge-ui/src/app/components/race-details/race-details.component.spec.ts

@ -6,10 +6,12 @@ describe('RaceDetailsComponent', () => {
let component: RaceDetailsComponent; let component: RaceDetailsComponent;
let fixture: ComponentFixture<RaceDetailsComponent>; let fixture: ComponentFixture<RaceDetailsComponent>;
beforeEach(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [RaceDetailsComponent] imports: [RaceDetailsComponent]
}); })
.compileComponents();
fixture = TestBed.createComponent(RaceDetailsComponent); fixture = TestBed.createComponent(RaceDetailsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

106
packages/bridge-ui/src/app/components/race-details/race-details.component.ts

@ -0,0 +1,106 @@
import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { RacersService } from '../../services/racers.service';
import { RaceResultService } from '../../services/race-result.service';
import { Race } from '../../models/race.model';
import { Racer } from '../../models/racer.model';
import { RaceEntry } from '../../models/raceEntry.model';
@Component({
selector: 'app-race-details',
standalone: true,
imports: [
CommonModule,
MatGridListModule,
MatIconModule,
MatTableModule,
MatButtonModule
],
templateUrl: './race-details.component.html',
styleUrl: './race-details.component.scss'
})
export class RaceDetailsComponent {
@Input() race?: Race;
racers: Map<string, Racer> = new Map<string, Racer>();
raceResults: Map<string, RaceEntry[]> = new Map<string, RaceEntry[]>;
displayedColumns: string[] = ['position', 'name', 'runTime', 'ghost'];
sortedResults: RaceEntry[] = [];
constructor(
private racersService: RacersService,
private raceResultService: RaceResultService,
) {}
ngOnInit() {
if( this.race == undefined) {
return;
}
for( let racer_id of this.race?.racers ) {
this.racersService.getRacer(racer_id).subscribe( data => {
if(data != undefined) {
this.racers.set(racer_id, data);
}
});
this.raceResultService.getRaceResultsForRacerInRace(racer_id, this.race.id).subscribe( data => {
if(data != undefined) {
this.raceResults.set(racer_id, data);
let result = this.getBestTimeForRacer(racer_id);
if (result != undefined) {
this.sortedResults.push(result)
this.sortedResults.sort((a, b) => {
if( a.time == b.time ) { return 0 };
if( a.time < b.time ) { return -1};
return 1;
})
this.sortedResults = [...this.sortedResults];
}
}
});
}
}
getBestTimeForRacer(racer_id: string): RaceEntry | undefined {
let results = this.raceResults.get(racer_id)
if (results == undefined ){
return undefined
}
let bestTime: number = 0xFFFFFFFF;
let bestResult = undefined
for(let result of results){
if(result.time < bestTime) {
bestTime = result.time
bestResult = result;
}
}
return bestResult
}
formatMilliseconds(milliseconds: number)
{
const minutes = Math.floor(milliseconds / (1000 * 60));
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
const remainingMilliseconds = milliseconds % 1000;
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(remainingMilliseconds).padStart(3, '0')}`;
}
getRacerName(racerId: string): string {
let racer = this.racers.get(racerId);
if( racer != undefined) {
return racer.name + " (" + racer.gameHandle + ")"
}
return ""
}
}

10
packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.html

@ -0,0 +1,10 @@
<a class="docs-guide-item ng-star-inserted" (click)="openNewSeasonDialog()">
<mat-card class="mat-mdc-card mdc-card docs-guide-card">
<mat-card-title class="mat-mdc-card-title">New Season</mat-card-title>
<mat-card-subtitle>Make it!</mat-card-subtitle>
<div class="docs-guide-card-divider"></div>
<mat-card-content class="mat-mdc-card-content docs-guide-card-summary">
<mat-icon>add</mat-icon>
</mat-card-content>
</mat-card>
</a>

32
packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.scss

@ -0,0 +1,32 @@
.docs-guide-card.mat-mdc-card {
text-align: center;
width: 240px;
height: 240px;
padding: 16px 0;
}
.docs-guide-item {
text-decoration: none;
display: flex;
margin: 15px;
}
.docs-guide-card .mat-mdc-card-title {
height: 33%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.docs-guide-card-divider {
width: 40%;
height: 7px;
margin: 15px auto;
}
mat-icon{
width: 50px;
height: 50px;
font-size: xxx-large;
}

23
packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SeasonCardNewComponent } from './season-card-new.component';
describe('SeasonCardNewComponent', () => {
let component: SeasonCardNewComponent;
let fixture: ComponentFixture<SeasonCardNewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SeasonCardNewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SeasonCardNewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

23
packages/bridge-ui/src/app/components/season-card-new/season-card-new.component.ts

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-season-card-new',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
],
templateUrl: './season-card-new.component.html',
styleUrl: './season-card-new.component.scss'
})
export class SeasonCardNewComponent {
openNewSeasonDialog() {
}
}

13
packages/bridge-ui/src/app/components/season-card/season-card.component.html

@ -0,0 +1,13 @@
<a class="docs-guide-item ng-star-inserted" href="/seasons/{{season?.id}}">
<mat-card class="mat-mdc-card mdc-card docs-guide-card">
@if (season != undefined) {
<mat-card-title class="mat-mdc-card-title">{{season.title}}</mat-card-title>
<mat-card-subtitle>{{season.subTitle}}</mat-card-subtitle>
<div class="docs-guide-card-divider"></div>
<mat-card-content class="mat-mdc-card-content docs-guide-card-summary">
<p>Start Date: {{getStartingDate()}}</p>
<p>Number of Races: {{season.races.length}}</p>
</mat-card-content>
}
</mat-card>
</a>

26
packages/bridge-ui/src/app/components/season-card/season-card.component.scss

@ -0,0 +1,26 @@
.docs-guide-card.mat-mdc-card {
text-align: center;
width: 240px;
height: 240px;
padding: 16px 0;
}
.docs-guide-item {
text-decoration: none;
display: flex;
margin: 15px;
}
.docs-guide-card .mat-mdc-card-title {
height: 33%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.docs-guide-card-divider {
width: 40%;
height: 7px;
margin: 15px auto;
}

10
src/app/components/season-card/season-card.component.spec.ts → packages/bridge-ui/src/app/components/season-card/season-card.component.spec.ts

@ -6,10 +6,12 @@ describe('SeasonCardComponent', () => {
let component: SeasonCardComponent; let component: SeasonCardComponent;
let fixture: ComponentFixture<SeasonCardComponent>; let fixture: ComponentFixture<SeasonCardComponent>;
beforeEach(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [SeasonCardComponent] imports: [SeasonCardComponent]
}); })
.compileComponents();
fixture = TestBed.createComponent(SeasonCardComponent); fixture = TestBed.createComponent(SeasonCardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

33
packages/bridge-ui/src/app/components/season-card/season-card.component.ts

@ -0,0 +1,33 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { Season } from '../../models/season.model';
@Component({
selector: 'app-season-card',
standalone: true,
imports: [
CommonModule,
MatCardModule
],
templateUrl: './season-card.component.html',
styleUrl: './season-card.component.scss'
})
export class SeasonCardComponent {
@Input() season?: Season;
getStartingDate() {
if(this.season != undefined){
let date = new Date(0);
date.setUTCSeconds(Number(this.season.startingDate));
return date.toUTCString();
}
return "";
}
}

23
packages/bridge-ui/src/app/components/season-standings/season-standings.component.html

@ -0,0 +1,23 @@
<div>
<table mat-table [dataSource]="sortedStandings" class="mat-elevation-z8">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element; let i = index">{{i + 1}}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Name </th>
<td mat-cell *matCellDef="let element"> {{getRacerName(element.id)}} </td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="points">
<th mat-header-cell *matHeaderCellDef>Points </th>
<td mat-cell *matCellDef="let element"> {{element.points}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

0
src/app/components/new-season-dialog/new-season-dialog.component.scss → packages/bridge-ui/src/app/components/season-standings/season-standings.component.scss

23
packages/bridge-ui/src/app/components/season-standings/season-standings.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SeasonStandingsComponent } from './season-standings.component';
describe('SeasonStandingsComponent', () => {
let component: SeasonStandingsComponent;
let fixture: ComponentFixture<SeasonStandingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SeasonStandingsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SeasonStandingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

155
packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts

@ -0,0 +1,155 @@
import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { RacersService } from '../../services/racers.service';
import { RaceResultService } from '../../services/race-result.service';
import { Race } from '../../models/race.model';
import { Racer } from '../../models/racer.model';
import { RaceEntry } from '../../models/raceEntry.model';
import { Season } from '../../models/season.model';
import { RacesService } from '../../services/races.service';
class SeasonStanding {
id: string = "";
points: number = 0;
constructor(_id: string) { this.id = _id;}
}
@Component({
selector: 'app-season-standings',
standalone: true,
imports: [
CommonModule,
MatTableModule,
],
templateUrl: './season-standings.component.html',
styleUrl: './season-standings.component.scss'
})
export class SeasonStandingsComponent {
@Input() season?: Season
races: Race[] = [];
seasonRacers: Map<string, Racer> = new Map<string, Racer>();
seasonRaceResults: Map<string, RaceEntry[]> = new Map<string, RaceEntry[]>;
seasonPoints: Map<string, SeasonStanding> = new Map<string, SeasonStanding>;
displayedColumns: string[] = ['position', 'name', 'points'];
sortedStandings!: SeasonStanding[];
constructor(
private racersService: RacersService,
private raceResultService: RaceResultService,
private racesService: RacesService,
) {}
ngOnInit() {
if(this.season == undefined){
return;
}
if(this.races == undefined) {
return;
}
for (let race_id of this.season.races) {
this.racesService.getRace(race_id).subscribe( race => {
if( race != undefined ){
this.races.push(race);
this.seasonRaceResults.set(race.id, []);
for( let racer_id of race.racers ) {
if( this.seasonRacers.has(racer_id) == false ) {
this.racersService.getRacer(racer_id).subscribe( data => {
if(data != undefined) {
this.seasonRacers.set(racer_id, data);
}
});
}
let thisRaceResults = this.seasonRaceResults.get(race.id);
this.raceResultService.getRaceResultsForRacerInRace(racer_id, race.id).subscribe( data => {
if(data != undefined) {
let bestResult = this.getBestTime(data);
if(thisRaceResults != undefined) {
thisRaceResults.push(bestResult);
}
this.calculateSeasonPoints();
}
});
}
}
});
}
}
getRacerName(racerId: string): string {
let racer = this.seasonRacers.get(racerId);
if( racer != undefined) {
return racer.name + " (" + racer.gameHandle + ")"
}
return ""
}
getBestTime(results: RaceEntry[]): RaceEntry {
let bestTime: number = 0xFFFFFFFF;
let bestResult = new RaceEntry();
for(let result of results){
if(result.time < bestTime) {
bestTime = result.time
bestResult = result;
}
}
return bestResult
}
calculateSeasonPoints() {
if(this.races == undefined) {
return;
}
let maxRacePoints = this.seasonRacers.size;
this.seasonPoints = new Map<string, SeasonStanding>();
for( let race of this.races) {
let availablePoints = maxRacePoints;
let thisRaceResults = this.seasonRaceResults.get(race.id);
if(thisRaceResults == undefined) {
continue
}
thisRaceResults.sort((a,b) =>{
return a.time - b.time;
})
for (let result of thisRaceResults) {
if (this.seasonPoints.has(result.racer_id) == false) {
this.seasonPoints.set(result.racer_id, new SeasonStanding(result.racer_id));
}
let currentPoints = this.seasonPoints.get(result.racer_id);
if( currentPoints == undefined ) {
continue;
}
currentPoints.points+=availablePoints;
availablePoints -=1;
}
}
this.sortedStandings = Array.from(this.seasonPoints.values()).sort((a, b) => {
return b.points - a.points;
})
this.sortedStandings = [...this.sortedStandings]
}
}

40
packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.html

@ -0,0 +1,40 @@
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" [matMenuTriggerFor]="menu">
<mat-icon class="icon">palette</mat-icon>
<mat-menu #menu="matMenu">
<button
*ngFor="let option of options"
mat-menu-item
(click)="changeTheme(option.value)">
<mat-icon
role="img"
svgicon="theme-example"
aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%"
height="100%"
viewBox="0 0 80 80"
fit=""
preserveAspectRatio="xMidYMid meet"
focusable="false">
<defs>
<path
d="M77.87 0C79.05 0 80 .95 80 2.13v75.74c0 1.17-.95 2.13-2.13 2.13H2.13C.96 80 0 79.04 0 77.87V2.13C0 .95.96 0 2.13 0h75.74z"
id="a">
</path>
<path
d="M54 40c3.32 0 6 2.69 6 6 0 1.2 0-1.2 0 0 0 3.31-2.68 6-6 6H26c-3.31 0-6-2.69-6-6 0-1.2 0 1.2 0 0 0-3.31 2.69-6 6-6h28z"
id="b">
</path>
<path d="M0 0h80v17.24H0V0z" id="c"></path>
</defs>
<use xlink:href="#a" [attr.fill]="option.backgroundColor"></use>
<use xlink:href="#b" [attr.fill]="option.buttonColor"></use>
<use xlink:href="#c" [attr.fill]="option.headingColor"></use>
</svg>
</mat-icon>
<span>{{ option.label }}</span>
</button>
</mat-menu>
</button>

0
src/app/components/season-list/season-list.component.scss → packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.scss

23
packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ThemeMenuComponent } from './theme-menu.component';
describe('ThemeMenuComponent', () => {
let component: ThemeMenuComponent;
let fixture: ComponentFixture<ThemeMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ThemeMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ThemeMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

33
packages/bridge-ui/src/app/components/theme-menu/theme-menu.component.ts

@ -0,0 +1,33 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
import { ThemeOption } from '../../models/theme-option.model';
import { ThemeService } from '../../services/theme.service';
@Component({
selector: 'app-theme-menu',
standalone: true,
imports: [
CommonModule,
MatIconModule,
MatMenuModule,
MatButtonModule
],
templateUrl: './theme-menu.component.html',
styleUrl: './theme-menu.component.scss'
})
export class ThemeMenuComponent {
constructor(private themeService: ThemeService) {}
@Input() options: Array<ThemeOption> = [];
@Output() themeChange: EventEmitter<string> = new EventEmitter<string>();
changeTheme(selectedTheme: string) {
this.themeChange.emit(selectedTheme);
}
}

25
packages/bridge-ui/src/app/components/top-bar/top-bar.component.html

@ -0,0 +1,25 @@
<mat-toolbar color="primary">
<button mat-flat-button color="primary" [routerLink]="['/seasons']">
<mat-icon>sports_esports</mat-icon>
Seasons
</button>
<button mat-flat-button color="primary" [routerLink]="['/getting-started']">
<mat-icon>help_center</mat-icon>
Getting Started
</button>
<span class="example-spacer"></span>
<app-theme-menu
[options] = "options"
(themeChange)="themeChangeHandler($event)">
</app-theme-menu>
@if(isAuthed() == false) {
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onLoginClick()">
<mat-icon>login</mat-icon>
</button>
}
@if(isAuthed()) {
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onProfile()">
<mat-icon>person_pin</mat-icon>
</button>
}
</mat-toolbar>

4
src/app/components/top-bar/top-bar.component.scss → packages/bridge-ui/src/app/components/top-bar/top-bar.component.scss

@ -1,3 +1,5 @@
@use '@angular/material' as mat;
.example-spacer { .example-spacer {
flex: 1 1 auto; flex: 1 1 auto;
} }

10
src/app/components/top-bar/top-bar.component.spec.ts → packages/bridge-ui/src/app/components/top-bar/top-bar.component.spec.ts

@ -6,10 +6,12 @@ describe('TopBarComponent', () => {
let component: TopBarComponent; let component: TopBarComponent;
let fixture: ComponentFixture<TopBarComponent>; let fixture: ComponentFixture<TopBarComponent>;
beforeEach(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [TopBarComponent] imports: [TopBarComponent]
}); })
.compileComponents();
fixture = TestBed.createComponent(TopBarComponent); fixture = TestBed.createComponent(TopBarComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

66
packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts

@ -0,0 +1,66 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable } from 'rxjs';
import { RouterModule } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatDialog } from '@angular/material/dialog';
import { ThemeMenuComponent } from '../theme-menu/theme-menu.component';
import { ThemeService } from '../../services/theme.service';
import { ThemeOption } from '../../models/theme-option.model';
import { LoginDialogComponent } from '../login-dialog/login-dialog.component';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-top-bar',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
ThemeMenuComponent
],
templateUrl: './top-bar.component.html',
styleUrl: './top-bar.component.scss'
})
export class TopBarComponent {
constructor(
private readonly themeService: ThemeService,
private authService: AuthService,
public dialog: MatDialog
) {}
options: Array<ThemeOption> = [];
async ngOnInit() {
this.themeService.getThemeOptions().subscribe(data => {
this.options = data;
});
}
themeChangeHandler(seletedTheme: string) {
this.themeService.setTheme(seletedTheme)
}
onLoginClick() {
this.dialog.open(LoginDialogComponent);
}
isAuthed() {
return this.authService.isAuthenticated();
}
onProfile() {
this.authService.testProfile().subscribe(data => {
console.log(data)
});
}
}

3
src/app/interceptors/auth-interceptor.ts → packages/bridge-ui/src/app/interceptors/auth.interceptor.ts

@ -7,8 +7,7 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, intercept(req: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> { next: HttpHandler): Observable<HttpEvent<any>> {
const idToken = localStorage.getItem("token");
const idToken = localStorage.getItem("id_token");
if (idToken) { if (idToken) {
const cloned = req.clone({ const cloned = req.clone({

10
packages/bridge-ui/src/app/models/race.model.ts

@ -0,0 +1,10 @@
export class Race{
id: string = "";
mapName: string = "";
mapUrl: string = "";
mapUID: string = "";
mapImgUrl: string = "";
startDate: Date = new Date(0);
endDate: Date = new Date(0);
racers: string[] = [];
}

7
packages/bridge-ui/src/app/models/raceEntry.model.ts

@ -0,0 +1,7 @@
export class RaceEntry {
id: string = "";
race_id: string = "";
racer_id: string = "";
replayPath: string = "";
time: number = 0;
}

5
packages/bridge-ui/src/app/models/racer.model.ts

@ -0,0 +1,5 @@
export class Racer {
id: string = "";
name: string = "";
gameHandle: string = "";
}

9
packages/bridge-ui/src/app/models/season.model.ts

@ -0,0 +1,9 @@
export class Season {
id: string = "0";
title: string = "";
subTitle: string = "";
startingDate: Date = new Date(Date.now());
races: string[] = [];
racers: string[] = [];
standings: string[] = [];
}

7
packages/bridge-ui/src/app/models/theme-option.model.ts

@ -0,0 +1,7 @@
export interface ThemeOption {
backgroundColor: string;
buttonColor: string;
headingColor: string;
label: string;
value: string;
}

23
packages/bridge-ui/src/app/pages/getting-started/getting-started.component.html

@ -0,0 +1,23 @@
<div class="docs-guide-wrapper">
<div class="docs-markdown">
<h1>Getting Started</h1>
<p>In order to take part in a trackmanai tournament you are going to need a few things.</p>
<p>Trackmanaia being the primary thing. You can get it from <a href="https://store.steampowered.com/app/2225070/Trackmania/">Steam</a>, <a href="https://store.epicgames.com/en-US/p/trackmania">Epic Store</a> or <a href="https://store.ubisoft.com/uk/trackmania-starter-access/5e8b58345cdf9a12c868c878.html">Ubisoft</a></p>
<p>You will need to purchase standard access to the game, which you can do <a href="https://store.ubisoft.com/uk/trackmania-standard-access-1-year/5e14dc8a5cdf9a1ec45ad81f.html">here</a> or <a href="https://store.epicgames.com/en-US/p/trackmania--standard-access-one-year">here</a>. It does go on sale now and again so keep an eye out for other offers.</p>
<br/>
<h1>Plugins</h1>
<p>You are going to need to install some plugins for the game to help facilitate the tournaments.</p>
<p>First you will need to heave ofer to <a href="OpenPlanet.dev">OpenPlanet.dev</a> and download the latest version of <a href="https://openplanet.dev/download/next">OpenPlanet for Trackmania</a>. Thats 1.26.5 at the time of writing.</p>
<p>Once that is installed you can boot the game and hit f3 to bring up the plugins bar.</p>
<p>From there open up the plugins manager and search fro "replay". You will need to install Autosave Ghosts and MLHook. This will auto save a replay for each successful race you complete automatically.</p>
<p>These replay will be used to record your best times during the tournament.</p>
<br/>
<h1>Replays</h1>
</div>
</div>

18
packages/bridge-ui/src/app/pages/getting-started/getting-started.component.scss

@ -0,0 +1,18 @@
mat-drawer-container,
mat-drawer-content,
mat-drawer {
height: calc(100vh - 56px);
}
mat-drawer {
width: 200px;
}
.docs-guide-wrapper{
padding: 20px 40px 0;
display: block;
}
h1{
text-align: left;
}

23
packages/bridge-ui/src/app/pages/getting-started/getting-started.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GettingStartedComponent } from './getting-started.component';
describe('GettingStartedComponent', () => {
let component: GettingStartedComponent;
let fixture: ComponentFixture<GettingStartedComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GettingStartedComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GettingStartedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

22
packages/bridge-ui/src/app/pages/getting-started/getting-started.component.ts

@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
@Component({
selector: 'app-getting-started',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatSidenavModule,
MatListModule,
],
templateUrl: './getting-started.component.html',
styleUrl: './getting-started.component.scss'
})
export class GettingStartedComponent {
}

1
packages/bridge-ui/src/app/pages/home/home.component.html

@ -0,0 +1 @@
<p>home works!</p>

0
src/app/components/season-standings-table/season-standings-table.component.scss → packages/bridge-ui/src/app/pages/home/home.component.scss

23
packages/bridge-ui/src/app/pages/home/home.component.spec.ts

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save