201 changed files with 42589 additions and 12322 deletions
@ -1,49 +1 @@ |
|||
# 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 |
|||
|
|||
# Uploads |
|||
/uploads |
|||
nedb-database/raceresults.db |
|||
nedb-database/racers.db |
|||
nedb-database/races.db |
|||
nedb-database/seasons.db |
|||
node_modules/ |
|||
@ -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 |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"cwd": "${workspaceFolder}/pakcages/bridge-ui/", |
|||
"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" |
|||
} |
|||
] |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -1,6 +0,0 @@ |
|||
function readStringAndAdvance(buffer, start, numBytes) |
|||
{ |
|||
let pos = start |
|||
let string = buffer.subarray(pos, pos+=numBytes); |
|||
return string.toString(), pos; |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"$schema": "node_modules/lerna/schemas/lerna-schema.json", |
|||
"version": "0.0.0" |
|||
} |
|||
File diff suppressed because it is too large
@ -1,59 +1,20 @@ |
|||
{ |
|||
"name": "bridge", |
|||
"version": "0.0.0", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"ng": "ng", |
|||
"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" |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"devDependencies": { |
|||
"@angular-devkit/build-angular": "^16.2.9", |
|||
"@angular/cli": "^16.2.9", |
|||
"@angular/compiler-cli": "^16.2.0", |
|||
"@types/jasmine": "~4.3.0", |
|||
"jasmine-core": "~4.6.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.1.3" |
|||
} |
|||
"lerna": "^7.4.2" |
|||
}, |
|||
"workspaces": [ |
|||
"packages/bridge-server", |
|||
"packages/bridge-ui", |
|||
"packages/bridge-shared" |
|||
] |
|||
} |
|||
|
|||
@ -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', |
|||
}, |
|||
}; |
|||
@ -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 |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"singleQuote": true, |
|||
"trailingComma": "all" |
|||
} |
|||
@ -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> |
|||
<!--[](https://opencollective.com/nest#backer) |
|||
[](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,0 +1,8 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/nest-cli", |
|||
"collection": "@nestjs/schematics", |
|||
"sourceRoot": "src", |
|||
"compilerOptions": { |
|||
"deleteOutDir": true |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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" |
|||
} |
|||
} |
|||
@ -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!'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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 }); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Injectable } from '@nestjs/common'; |
|||
|
|||
@Injectable() |
|||
export class AppService { |
|||
getHello(): string { |
|||
return 'Hello World!'; |
|||
} |
|||
} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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 {} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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), |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export const jwtConstants = { |
|||
secret: 'ThisLittlePiggy', |
|||
}; |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,9 @@ |
|||
import { Controller, Get } from '@nestjs/common'; |
|||
|
|||
@Controller('seasons') |
|||
export class SeasonsController { |
|||
@Get() |
|||
findAll(): string { |
|||
return '' |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { UsersService } from './users.service'; |
|||
|
|||
@Module({ |
|||
providers: [UsersService], |
|||
exports: [UsersService], |
|||
}) |
|||
export class UsersModule {} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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!'); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"moduleFileExtensions": ["js", "json", "ts"], |
|||
"rootDir": ".", |
|||
"testEnvironment": "node", |
|||
"testRegex": ".e2e-spec.ts$", |
|||
"transform": { |
|||
"^.+\\.(t|j)s$": "ts-jest" |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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,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,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" |
|||
} |
|||
] |
|||
} |
|||
@ -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 |
|||
|
|||
@ -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,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); |
|||
} |
|||
} |
|||
@ -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}, |
|||
] |
|||
}; |
|||
@ -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 }, |
|||
]; |
|||
@ -0,0 +1 @@ |
|||
<p>bottom-bar works!</p> |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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 { |
|||
|
|||
} |
|||
@ -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> |
|||
@ -0,0 +1,12 @@ |
|||
.signInForm { |
|||
width: 400px; |
|||
padding: 12px 24px 24px; |
|||
|
|||
igx-input-group + igx-input-group { |
|||
margin-top: 24px; |
|||
} |
|||
} |
|||
|
|||
.full-width { |
|||
width: 100%; |
|||
} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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(); |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
@ -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,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 "" |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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() { |
|||
|
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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 ""; |
|||
} |
|||
|
|||
} |
|||
@ -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,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(); |
|||
}); |
|||
}); |
|||
@ -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] |
|||
} |
|||
} |
|||
@ -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,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(); |
|||
}); |
|||
}); |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -1,3 +1,5 @@ |
|||
@use '@angular/material' as mat; |
|||
|
|||
.example-spacer { |
|||
flex: 1 1 auto; |
|||
} |
|||
@ -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) |
|||
}); |
|||
} |
|||
} |
|||
@ -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[] = []; |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
export class RaceEntry { |
|||
id: string = ""; |
|||
race_id: string = ""; |
|||
racer_id: string = ""; |
|||
replayPath: string = ""; |
|||
time: number = 0; |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export class Racer { |
|||
id: string = ""; |
|||
name: string = ""; |
|||
gameHandle: string = ""; |
|||
} |
|||
@ -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[] = []; |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
export interface ThemeOption { |
|||
backgroundColor: string; |
|||
buttonColor: string; |
|||
headingColor: string; |
|||
label: string; |
|||
value: string; |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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 { |
|||
|
|||
} |
|||
@ -0,0 +1 @@ |
|||
<p>home works!</p> |
|||
@ -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…
Reference in new issue