Database Connection Update #4
330
.drone.yml
330
.drone.yml
@@ -3,6 +3,34 @@ type: docker
|
|||||||
name: frontend
|
name: frontend
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Restore cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
restore: true
|
||||||
|
debug: true
|
||||||
|
filename: frontend.tar
|
||||||
|
workdir: frontend/
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd frontend
|
||||||
|
- yarn
|
||||||
|
|
||||||
|
# - name: Run unit tests
|
||||||
|
# image: node
|
||||||
|
# environment:
|
||||||
|
# CI: true
|
||||||
|
# commands:
|
||||||
|
# - cd frontend
|
||||||
|
# - yarn test
|
||||||
|
|
||||||
- name: Build and push frontend image
|
- name: Build and push frontend image
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
@@ -12,20 +40,185 @@ steps:
|
|||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
dockerfile: frontend/Dockerfile
|
dockerfile: frontend/Dockerfile
|
||||||
context: frontend/
|
context: frontend/
|
||||||
|
use_cache: true
|
||||||
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend
|
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend
|
||||||
registry: docker.humenius.me
|
registry: docker.humenius.me
|
||||||
tags: ["latest", "${DRONE_SEMVER}"]
|
tags: ["latest", "${DRONE_SEMVER}"]
|
||||||
|
|
||||||
|
- name: Rebuild cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
rebuild: true
|
||||||
|
debug: true
|
||||||
|
filename: frontend.tar
|
||||||
|
mount:
|
||||||
|
- frontend/node_modules
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: Flush cache
|
||||||
|
image: plugins/s3-cache:1
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
flush: true
|
||||||
|
flush_age: 14
|
||||||
|
debug: true
|
||||||
|
filename: frontend.tar
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
|
branch:
|
||||||
|
- release/*
|
||||||
ref:
|
ref:
|
||||||
- refs/tags/* # only trigger when tagging
|
- refs/tags/* # only trigger when tagging
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: frontend-dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Restore cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
restore: true
|
||||||
|
debug: true
|
||||||
|
filename: frontend-dev.tar
|
||||||
|
workdir: frontend/
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd frontend
|
||||||
|
- ls -la
|
||||||
|
- yarn
|
||||||
|
|
||||||
|
# - name: Run unit tests
|
||||||
|
# image: node
|
||||||
|
# environment:
|
||||||
|
# CI: true
|
||||||
|
# commands:
|
||||||
|
# - cd frontend
|
||||||
|
# - yarn test
|
||||||
|
|
||||||
|
- name: Build and push frontend dev image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
dockerfile: frontend/Dockerfile
|
||||||
|
context: frontend/
|
||||||
|
use_cache: true
|
||||||
|
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend
|
||||||
|
registry: docker.humenius.me
|
||||||
|
tags: ["dev-${DRONE_COMMIT_SHA}", "dev-latest"]
|
||||||
|
|
||||||
|
- name: Rebuild cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
rebuild: true
|
||||||
|
debug: true
|
||||||
|
filename: frontend-dev.tar
|
||||||
|
mount:
|
||||||
|
- frontend/node_modules
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: Flush cache
|
||||||
|
image: plugins/s3-cache:1
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
flush: true
|
||||||
|
flush_age: 14
|
||||||
|
debug: true
|
||||||
|
filename: frontend-dev.tar
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
exclude:
|
||||||
|
branch:
|
||||||
|
- release/*
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: backend
|
name: backend
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Restore cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
restore: true
|
||||||
|
debug: true
|
||||||
|
filename: backend.tar
|
||||||
|
workdir: backend/
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- ls -la
|
||||||
|
- npm run test:cov
|
||||||
|
|
||||||
|
# - name: Run E2E tests
|
||||||
|
# image: node
|
||||||
|
# - cd backend
|
||||||
|
# - npm run test:e2e
|
||||||
|
|
||||||
- name: Build and push backend image
|
- name: Build and push backend image
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
@@ -34,11 +227,146 @@ steps:
|
|||||||
password:
|
password:
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
|
use_cache: true
|
||||||
context: backend/
|
context: backend/
|
||||||
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-backend
|
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-backend
|
||||||
registry: docker.humenius.me
|
registry: docker.humenius.me
|
||||||
tags: ["latest", "${DRONE_SEMVER}"]
|
tags: ["latest", "${DRONE_SEMVER}"]
|
||||||
|
|
||||||
|
- name: Rebuild cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
rebuild: true
|
||||||
|
debug: true
|
||||||
|
filename: backend.tar
|
||||||
|
mount:
|
||||||
|
- backend/node_modules
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: Flush cache
|
||||||
|
image: plugins/s3-cache:1
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
flush: true
|
||||||
|
flush_age: 14
|
||||||
|
debug: true
|
||||||
|
filename: backend.tar
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
|
branch:
|
||||||
|
- release/*
|
||||||
ref:
|
ref:
|
||||||
- refs/tags/* # only trigger when tagging
|
- refs/tags/* # only trigger when tagging
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: backend-dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Restore cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
restore: true
|
||||||
|
debug: true
|
||||||
|
filename: backend-dev.tar
|
||||||
|
workdir: backend/
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- npm run test:cov
|
||||||
|
|
||||||
|
# - name: Run E2E tests
|
||||||
|
# image: node
|
||||||
|
# - cd backend
|
||||||
|
# - npm run test:e2e
|
||||||
|
|
||||||
|
- name: Build and push backend dev image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
context: backend/
|
||||||
|
use_cache: true
|
||||||
|
repo: docker.humenius.me/humenius/ts-onlinetime-ranks-backend
|
||||||
|
registry: docker.humenius.me
|
||||||
|
tags: ["dev-${DRONE_COMMIT_SHA}", "dev-latest"]
|
||||||
|
|
||||||
|
- name: Rebuild cache
|
||||||
|
image: plugins/s3-cache
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
rebuild: true
|
||||||
|
debug: true
|
||||||
|
filename: backend-dev.tar
|
||||||
|
mount:
|
||||||
|
- backend/node_modules
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: Flush cache
|
||||||
|
image: plugins/s3-cache:1
|
||||||
|
settings:
|
||||||
|
pull: true
|
||||||
|
endpoint: https://storage.humenius.me
|
||||||
|
access_key:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
flush: true
|
||||||
|
flush_age: 14
|
||||||
|
debug: true
|
||||||
|
filename: backend-dev.tar
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
exclude:
|
||||||
|
branch:
|
||||||
|
- release/*
|
||||||
2
backend/.dockerignore
Normal file
2
backend/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
@@ -1,15 +1,37 @@
|
|||||||
FROM node:14.3.0-alpine AS builder
|
# FROM node:14.3.0-alpine AS builder
|
||||||
|
# WORKDIR /app
|
||||||
|
# COPY package.json .
|
||||||
|
# COPY package-lock.json .
|
||||||
|
# RUN npm install
|
||||||
|
# COPY . .
|
||||||
|
# RUN npm run build
|
||||||
|
|
||||||
|
# FROM node:14.3.0-alpine
|
||||||
|
# WORKDIR /app
|
||||||
|
# COPY --from=builder /app/dist .
|
||||||
|
|
||||||
|
# # ENV DATABASE_URL
|
||||||
|
|
||||||
|
# EXPOSE 3500
|
||||||
|
# CMD ["node", "main.js"]
|
||||||
|
|
||||||
|
FROM node:14.3.0-alpine as builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json .
|
|
||||||
COPY package-lock.json .
|
ENV DATABASE_URL="mysql://dummy:user@localhost:1234/db"
|
||||||
RUN npm install
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM node:14.3.0-alpine
|
FROM node:14.3.0-alpine
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /app .
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/package*.json ./
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
EXPOSE 3500
|
EXPOSE 3500
|
||||||
CMD ["npm", "run", "start:prod"]
|
CMD [ "npm", "run", "start:prod" ]
|
||||||
|
|
||||||
|
|||||||
3434
backend/package-lock.json
generated
3434
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,19 +20,21 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^7.0.0",
|
"@nestjs/common": "^7.6.5",
|
||||||
"@nestjs/core": "^7.0.0",
|
"@nestjs/core": "^7.6.5",
|
||||||
"@nestjs/platform-express": "^7.0.0",
|
"@nestjs/platform-express": "^7.6.5",
|
||||||
"node-fetch": "^2.6.0",
|
"@prisma/client": "^2.14.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^6.5.4",
|
"rxjs": "^6.5.4",
|
||||||
"winston": "^3.2.1"
|
"winston": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^7.0.0",
|
"@nestjs/cli": "^7.5.4",
|
||||||
"@nestjs/schematics": "^7.0.0",
|
"@nestjs/schematics": "^7.2.6",
|
||||||
"@nestjs/testing": "^7.0.0",
|
"@nestjs/testing": "^7.6.5",
|
||||||
|
"@prisma/cli": "^2.14.0",
|
||||||
"@types/express": "^4.17.3",
|
"@types/express": "^4.17.3",
|
||||||
"@types/jest": "25.1.4",
|
"@types/jest": "25.1.4",
|
||||||
"@types/node": "^13.9.1",
|
"@types/node": "^13.9.1",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-prettier": "^6.10.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.20.1",
|
||||||
"jest": "^25.1.0",
|
"jest": "^25.1.0",
|
||||||
|
"jest-mock-extended": "^1.0.10",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"ts-jest": "^26.1.1",
|
"ts-jest": "^26.1.1",
|
||||||
|
|||||||
48
backend/prisma/schema.prisma
Normal file
48
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "mysql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ranks {
|
||||||
|
entry_id Int @id @default(autoincrement())
|
||||||
|
season_id Int
|
||||||
|
rank_id Int @unique
|
||||||
|
rank_name String
|
||||||
|
seasons seasons @relation(fields: [season_id], references: [season_id])
|
||||||
|
timetracker timetracker[]
|
||||||
|
|
||||||
|
@@unique([season_id, rank_id], name: "ranks_season_id_rank_id_uindex")
|
||||||
|
}
|
||||||
|
|
||||||
|
model seasons {
|
||||||
|
season_id Int @id @default(autoincrement())
|
||||||
|
start_date DateTime @default(now())
|
||||||
|
end_date DateTime?
|
||||||
|
ranks ranks[]
|
||||||
|
timetracker timetracker[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model timetracker {
|
||||||
|
entry_id Int @id @default(autoincrement())
|
||||||
|
user_uid String
|
||||||
|
season_id Int
|
||||||
|
rank_id Int?
|
||||||
|
time Int
|
||||||
|
ranks ranks? @relation(fields: [rank_id], references: [rank_id])
|
||||||
|
seasons seasons @relation(fields: [season_id], references: [season_id])
|
||||||
|
user user @relation(fields: [user_uid], references: [uid])
|
||||||
|
|
||||||
|
@@unique([user_uid, season_id], name: "timetracker_user_uid_season_id_uindex")
|
||||||
|
@@index([rank_id], name: "timetracker_ranks_rank_id_fk")
|
||||||
|
@@index([season_id], name: "timetracker_seasons_season_id_fk")
|
||||||
|
}
|
||||||
|
|
||||||
|
model user {
|
||||||
|
uid String @id
|
||||||
|
name String?
|
||||||
|
timetracker timetracker[]
|
||||||
|
}
|
||||||
3
backend/src/api/predicate.ts
Normal file
3
backend/src/api/predicate.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface Predicate<T, O> {
|
||||||
|
process(input: T): O;
|
||||||
|
}
|
||||||
13
backend/src/api/sort.ts
Normal file
13
backend/src/api/sort.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class Sort {
|
||||||
|
public static descending = (a: number, b: number) => {
|
||||||
|
if (a < b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a > b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
25
backend/src/api/timeutil.spec.ts
Normal file
25
backend/src/api/timeutil.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { TimeUtil } from './timeutil';
|
||||||
|
|
||||||
|
describe('TimeUtil', () => {
|
||||||
|
it('should humanize raw seconds', () => {
|
||||||
|
// const rawTime = 12345;
|
||||||
|
// const expected = "0d 3h 25m 45s";
|
||||||
|
const input = {
|
||||||
|
rawTime: [
|
||||||
|
12345,
|
||||||
|
123,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
"0d 3h 25m 45s",
|
||||||
|
"0d 0h 2m 3s",
|
||||||
|
"0d 0h 0m 4s"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < input.rawTime.length; i++) {
|
||||||
|
const actual = TimeUtil.humanize(input.rawTime[i]);
|
||||||
|
expect(actual).toEqual(input.expected[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
10
backend/src/api/timeutil.ts
Normal file
10
backend/src/api/timeutil.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class TimeUtil {
|
||||||
|
public static humanize = (seconds: number) => {
|
||||||
|
const d = Math.floor(seconds / (3600 * 24));
|
||||||
|
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
return `${d}d ${h}h ${m}m ${s}s`;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HostParam,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TableEntry } from "./models/TableEntry";
|
import { DatabaseService } from './database/database.service';
|
||||||
import { SinusBotService } from "./services/sinusbot.service";
|
import logger from './logger/Logger';
|
||||||
|
import { TableEntry } from './models/TableEntry';
|
||||||
|
import { SinusBotService } from './services/sinusbot.service';
|
||||||
|
import { UserStatsResponse } from './models/aliases';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly appService: AppService,
|
private readonly appService: AppService,
|
||||||
private readonly sinusBotService: SinusBotService
|
private readonly sinusBotService: SinusBotService,
|
||||||
|
private readonly databaseService: DatabaseService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -15,8 +27,21 @@ export class AppController {
|
|||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/stats')
|
@Get('/stats-old')
|
||||||
async getStats(): Promise<TableEntry[]> {
|
async getStatsOld(): Promise<TableEntry[]> {
|
||||||
return await this.sinusBotService.fetchStats();
|
return await this.sinusBotService.fetchStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/stats/season/:id?')
|
||||||
|
async getStats(@Param('id') id?: string): Promise<UserStatsResponse> {
|
||||||
|
return await this.databaseService
|
||||||
|
.fetchStats(id)
|
||||||
|
.catch(err => {
|
||||||
|
logger.error(`Error occured when fetching stats.`, err);
|
||||||
|
throw new HttpException(
|
||||||
|
'Error when fetching stats. Contact administrator or try again later!',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { SinusBotService } from "./services/sinusbot.service";
|
import { SinusBotService } from './services/sinusbot.service';
|
||||||
|
import { DatabaseService } from './database/database.service';
|
||||||
|
import { LoggerMiddleware } from './logger.middleware';
|
||||||
|
import { PrismaService } from './prisma/prisma.service';
|
||||||
|
import { TimeTrackerPredicate } from './database/timetracking.predicate';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, SinusBotService],
|
providers: [
|
||||||
|
AppService,
|
||||||
|
SinusBotService,
|
||||||
|
DatabaseService,
|
||||||
|
PrismaService,
|
||||||
|
TimeTrackerPredicate,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer): any {
|
||||||
|
consumer.apply(LoggerMiddleware).forRoutes('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
69
backend/src/database/database.service.spec.ts
Normal file
69
backend/src/database/database.service.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { seasons } from '@prisma/client';
|
||||||
|
import { DatabaseService } from './database.service';
|
||||||
|
import { mockReset, MockProxy, mockDeep } from 'jest-mock-extended';
|
||||||
|
import { SeasonInfo } from '../models/aliases';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { TimeTrackerPredicate } from './timetracking.predicate';
|
||||||
|
import { mocked } from 'ts-jest';
|
||||||
|
|
||||||
|
describe('DatabaseService', () => {
|
||||||
|
let service: DatabaseService;
|
||||||
|
const mockedPrismaService: MockProxy<PrismaService> = mockDeep<PrismaService>();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockReset(mockedPrismaService);
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [DatabaseService, PrismaService, TimeTrackerPredicate],
|
||||||
|
})
|
||||||
|
.overrideProvider(PrismaService)
|
||||||
|
.useValue(mockedPrismaService)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
service = module.get<DatabaseService>(DatabaseService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO This will be skipped as Prisma__#client can barely be mocked.
|
||||||
|
// it('fetch season info with season ID given', async () => {
|
||||||
|
// const seasonIdToBeTested = 1;
|
||||||
|
// const mockedValue: seasons = {
|
||||||
|
// season_id: 2,
|
||||||
|
// start_date: new Date('1995-12-17T03:24:00'),
|
||||||
|
// end_date: null
|
||||||
|
// };
|
||||||
|
// const mockedMaxSeasonId = {
|
||||||
|
// max: {
|
||||||
|
// season_id: 2
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// mockedPrismaService.seasons.findUnique.calledWith({
|
||||||
|
// where: {
|
||||||
|
// season_id: Number(seasonIdToBeTested)
|
||||||
|
// }
|
||||||
|
// }).mockImplementation((subset) => {
|
||||||
|
// return new Promise(resolve => resolve(mockedValue));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// mockedPrismaService.seasons.aggregate.calledWith({
|
||||||
|
// max: {
|
||||||
|
// season_id: true
|
||||||
|
// }
|
||||||
|
// }).mockImplementation(() => Promise.resolve(mockedMaxSeasonId));
|
||||||
|
//
|
||||||
|
// const expected: SeasonInfo = {
|
||||||
|
// season_id: 2,
|
||||||
|
// maxSeasonId: 2,
|
||||||
|
// start_date: new Date('1995-12-17T03:24:00'),
|
||||||
|
// end_date: null
|
||||||
|
// }
|
||||||
|
// const actual = service.fetchSeasonInfos();
|
||||||
|
//
|
||||||
|
// await expect(actual).resolves.toEqual(expected);
|
||||||
|
// });
|
||||||
|
});
|
||||||
87
backend/src/database/database.service.ts
Normal file
87
backend/src/database/database.service.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TimeTrackerPredicate } from './timetracking.predicate';
|
||||||
|
import {
|
||||||
|
SeasonInfo,
|
||||||
|
TimeTrackerStats,
|
||||||
|
UserStatsResponse,
|
||||||
|
} from '../models/aliases';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { seasons } from '@prisma/client';
|
||||||
|
import logger from 'src/logger/Logger';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DatabaseService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaClient: PrismaService,
|
||||||
|
private readonly timetrackerPredicate: TimeTrackerPredicate,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public fetchSeasonInfos = async (seasonId?: string): Promise<SeasonInfo> => {
|
||||||
|
let maxSeasonId: number;
|
||||||
|
|
||||||
|
return this.fetchMaxSeasonId().then(value => {
|
||||||
|
maxSeasonId = value;
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.then(value => this.prismaClient.seasons.findUnique({
|
||||||
|
where: {
|
||||||
|
season_id: Number(seasonId ?? value)
|
||||||
|
}
|
||||||
|
}) as Promise<seasons>)
|
||||||
|
.then((result: SeasonInfo) => {
|
||||||
|
result.maxSeasonId = maxSeasonId;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public fetchStats = async (seasonId?: string): Promise<UserStatsResponse> => {
|
||||||
|
let response;
|
||||||
|
return this.fetchSeasonInfos(seasonId)
|
||||||
|
.then(result => {
|
||||||
|
response = {
|
||||||
|
seasonId: result.season_id,
|
||||||
|
maxSeasonId: result.maxSeasonId,
|
||||||
|
dates: {
|
||||||
|
start: result.start_date,
|
||||||
|
end: result.end_date,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return result.season_id.toString();
|
||||||
|
})
|
||||||
|
.then(this.fetchTimeTrackerStats)
|
||||||
|
.then(this.timetrackerPredicate.process)
|
||||||
|
.then(result => {
|
||||||
|
response.stats = result;
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTimeTrackerStats: (string) => Promise<TimeTrackerStats[]> = (
|
||||||
|
seasonId: string,
|
||||||
|
) =>
|
||||||
|
this.prismaClient.timetracker.findMany({
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
ranks: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||||
|
season_id: Number(seasonId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMaxSeasonId: () => Promise<number> = () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
this.prismaClient.seasons
|
||||||
|
.aggregate({
|
||||||
|
max: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||||
|
season_id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(result => result.max.season_id)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
71
backend/src/database/timetracking.predicate.spec.ts
Normal file
71
backend/src/database/timetracking.predicate.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { TimeTrackerPredicate } from './timetracking.predicate';
|
||||||
|
import { TimeTrackerStats } from '../models/aliases';
|
||||||
|
|
||||||
|
describe('TimeTrackerPredicate', () => {
|
||||||
|
let timeTrackerPredicate: TimeTrackerPredicate;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
timeTrackerPredicate = new TimeTrackerPredicate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(timeTrackerPredicate).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process UserStats properly to TableEntry', () => {
|
||||||
|
const input: TimeTrackerStats[] = [
|
||||||
|
{
|
||||||
|
entry_id: 1,
|
||||||
|
user_uid: 'TEST_UUID1',
|
||||||
|
season_id: 1,
|
||||||
|
rank_id: 1,
|
||||||
|
time: 12345,
|
||||||
|
user: {
|
||||||
|
uid: 'TEST_UUID1',
|
||||||
|
name: 'Test User 1'
|
||||||
|
},
|
||||||
|
ranks: {
|
||||||
|
entry_id: 1,
|
||||||
|
season_id: 1,
|
||||||
|
rank_id: 1,
|
||||||
|
rank_name: 'Test Rank 1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry_id: 2,
|
||||||
|
user_uid: 'TEST_UUID2',
|
||||||
|
season_id: 1,
|
||||||
|
rank_id: 2,
|
||||||
|
time: 123455,
|
||||||
|
user: {
|
||||||
|
uid: 'TEST_UUID2',
|
||||||
|
name: 'Test User 2'
|
||||||
|
},
|
||||||
|
ranks: {
|
||||||
|
entry_id: 1,
|
||||||
|
season_id: 1,
|
||||||
|
rank_id: 2,
|
||||||
|
rank_name: 'Test Rank 2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
name: 'Test User 2',
|
||||||
|
onlineTime: "1d 10h 17m 35s",
|
||||||
|
rank: 'Test Rank 2',
|
||||||
|
rawTime: 123455
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Test User 1',
|
||||||
|
onlineTime: "0d 3h 25m 45s",
|
||||||
|
rank: 'Test Rank 1',
|
||||||
|
rawTime: 12345
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actual = timeTrackerPredicate.process(input);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
})
|
||||||
|
});
|
||||||
24
backend/src/database/timetracking.predicate.ts
Normal file
24
backend/src/database/timetracking.predicate.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TimeTrackerStats } from '../models/aliases';
|
||||||
|
import { Predicate } from '../api/predicate';
|
||||||
|
import { TableEntry } from '../models/TableEntry';
|
||||||
|
import { TimeUtil } from '../api/timeutil';
|
||||||
|
import { Sort } from '../api/sort';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimeTrackerPredicate
|
||||||
|
implements Predicate<TimeTrackerStats[], TableEntry[]> {
|
||||||
|
process(input: TimeTrackerStats[]): TableEntry[] {
|
||||||
|
return input
|
||||||
|
.filter((userStats: TimeTrackerStats) => userStats.time != null)
|
||||||
|
.map((userStats: TimeTrackerStats) => {
|
||||||
|
return {
|
||||||
|
name: userStats.user.name,
|
||||||
|
rawTime: userStats.time,
|
||||||
|
onlineTime: TimeUtil.humanize(userStats.time),
|
||||||
|
rank: userStats.ranks.rank_name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((lhs, rhs) => Sort.descending(lhs.rawTime, rhs.rawTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/src/logger.middleware.ts
Normal file
22
backend/src/logger.middleware.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import logger from 'src/logger/Logger';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoggerMiddleware implements NestMiddleware {
|
||||||
|
use(request: Request, response: Response, next: NextFunction): void {
|
||||||
|
const { ip, method, originalUrl: url } = request;
|
||||||
|
const userAgent = request.get('user-agent') || '';
|
||||||
|
|
||||||
|
response.on('finish', () => {
|
||||||
|
const { statusCode } = response;
|
||||||
|
const contentLength = response.get('content-length');
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
|
|
||||||
const transports = {
|
const transports = {
|
||||||
console: new winston.transports.Console()
|
console: new winston.transports.Console(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
transports: [transports.console]
|
transports: [transports.console],
|
||||||
})
|
});
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ interface RequestError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RequestErrorConstructor extends ErrorConstructor {
|
interface RequestErrorConstructor extends ErrorConstructor {
|
||||||
new(message?: string): RequestError;
|
new (message?: string): RequestError;
|
||||||
(message?: string): RequestError;
|
(message?: string): RequestError;
|
||||||
readonly prototype: RequestError;
|
readonly prototype: RequestError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface TableEntry {
|
export interface TableEntry {
|
||||||
name: string;
|
name: string;
|
||||||
onlineTime: string;
|
rank: string;
|
||||||
|
onlineTime: string;
|
||||||
}
|
}
|
||||||
|
|||||||
6
backend/src/models/aliases.ts
Normal file
6
backend/src/models/aliases.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ranks, seasons, timetracker, user } from '@prisma/client';
|
||||||
|
import { TableEntry } from './TableEntry';
|
||||||
|
|
||||||
|
export type TimeTrackerStats = timetracker & { user: user; ranks: ranks };
|
||||||
|
export type SeasonInfo = seasons & { maxSeasonId: number };
|
||||||
|
export type UserStatsResponse = SeasonInfo & TableEntry[];
|
||||||
14
backend/src/prisma/prisma.service.ts
Normal file
14
backend/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient
|
||||||
|
implements OnModuleInit, OnModuleDestroy {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { TableEntry } from "../models/TableEntry";
|
import { TableEntry } from '../models/TableEntry';
|
||||||
import { TunakillUser } from "../models/TunakillUser";
|
import { TunakillUser } from '../models/TunakillUser';
|
||||||
import {HttpException, HttpStatus, Injectable} from "@nestjs/common";
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import {TunakillLogin} from "../models/TunakillLogin";
|
import { TunakillLogin } from '../models/TunakillLogin';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import logger from "../logger/Logger";
|
import logger from '../logger/Logger';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SinusBotService {
|
export class SinusBotService {
|
||||||
private host = process.env.HOST
|
private host = process.env.HOST;
|
||||||
private credentials = {
|
private credentials = {
|
||||||
username: process.env.SINUSBOT_USER,
|
username: process.env.SINUSBOT_USER,
|
||||||
password: process.env.SINUSBOT_PASSWORD
|
password: process.env.SINUSBOT_PASSWORD,
|
||||||
}
|
};
|
||||||
|
|
||||||
private botInfo = {
|
private botInfo = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
instanceId: process.env.SINUSBOT_INSTANCEID
|
instanceId: process.env.SINUSBOT_INSTANCEID,
|
||||||
}
|
};
|
||||||
|
|
||||||
private botIdURL = `${this.host}/api/v1/botId`;
|
private botIdURL = `${this.host}/api/v1/botId`;
|
||||||
private tunaKillURL = `${this.host}/api/v1/bot/i/${this.botInfo.instanceId}/event/tunakill_rank_all_user`;
|
private tunaKillURL = `${this.host}/api/v1/bot/i/${this.botInfo.instanceId}/event/tunakill_rank_all_user`;
|
||||||
@@ -34,23 +34,30 @@ export class SinusBotService {
|
|||||||
// if (this.bearer == null) {
|
// if (this.bearer == null) {
|
||||||
logger.info(`Hey! I'm trying to get my Bearer token!`);
|
logger.info(`Hey! I'm trying to get my Bearer token!`);
|
||||||
await this.login()
|
await this.login()
|
||||||
.then(token => this.bearer = token)
|
.then(token => (this.bearer = token))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Seems like I have my Bearer token!
|
`Seems like I have my Bearer token!
|
||||||
Looks like it's ${this.bearer == null ? 'undefined' : 'not undefined'}`
|
Looks like it's ${
|
||||||
|
this.bearer == null ? 'undefined' : 'not undefined'
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// }
|
// }
|
||||||
|
|
||||||
logger.info(`I try to fetch user data now! The URL is called ${this.tunaKillURL}`);
|
logger.info(
|
||||||
|
`I try to fetch user data now! The URL is called ${this.tunaKillURL}`,
|
||||||
|
);
|
||||||
return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer))
|
return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer))
|
||||||
.then(res => this.checkStatus(res))
|
.then(res => this.checkStatus(res))
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => this.consumeTunakillResponse(data))
|
.then(data => this.consumeTunakillResponse(data))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error(`I couldn't fetch user data.`, error);
|
logger.error(`I couldn't fetch user data.`, error);
|
||||||
throw this.createHttpException(HttpStatus.INTERNAL_SERVER_ERROR, error.message);
|
throw this.createHttpException(
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,12 +68,15 @@ export class SinusBotService {
|
|||||||
if (this.botInfo.id == null) {
|
if (this.botInfo.id == null) {
|
||||||
logger.debug(`I have to fetch a bot ID before I can continue!`);
|
logger.debug(`I have to fetch a bot ID before I can continue!`);
|
||||||
await this.fetchDefaultBotId()
|
await this.fetchDefaultBotId()
|
||||||
.then(defaultBotId => this.botInfo.id = defaultBotId)
|
.then(defaultBotId => (this.botInfo.id = defaultBotId))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.warn(`I couldn't retrieve SinusBot bot information. Login is likely to fail!`, error);
|
logger.warn(
|
||||||
|
`I couldn't retrieve SinusBot bot information. Login is likely to fail!`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw this.createHttpException(
|
throw this.createHttpException(
|
||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
`Could not fetch enough bot information for further requests.`
|
`Could not fetch enough bot information for further requests.`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
logger.info(`The bot ID now is ${this.botInfo.id}`);
|
logger.info(`The bot ID now is ${this.botInfo.id}`);
|
||||||
@@ -75,20 +85,23 @@ export class SinusBotService {
|
|||||||
const body: TunakillLogin = {
|
const body: TunakillLogin = {
|
||||||
username: this.credentials.username,
|
username: this.credentials.username,
|
||||||
password: this.credentials.password,
|
password: this.credentials.password,
|
||||||
botId: this.botInfo.id
|
botId: this.botInfo.id,
|
||||||
}
|
};
|
||||||
|
|
||||||
logger.info(`Logging in for Bearer token!`)
|
logger.info(`Logging in for Bearer token!`);
|
||||||
return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body)))
|
return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body)))
|
||||||
.then(res => this.checkStatus(res))
|
.then(res => this.checkStatus(res))
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => data.token)
|
.then(data => data.token)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error(`Oh oh! Something went wrong while fetching Bearer token.`, error);
|
logger.error(
|
||||||
|
`Oh oh! Something went wrong while fetching Bearer token.`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw this.createHttpException(
|
throw this.createHttpException(
|
||||||
HttpStatus.UNAUTHORIZED,
|
HttpStatus.UNAUTHORIZED,
|
||||||
`Fetching Bearer token for Sinusbot failed.
|
`Fetching Bearer token for Sinusbot failed.
|
||||||
Please refresh page or try again later!`
|
Please refresh page or try again later!`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,39 +123,56 @@ export class SinusBotService {
|
|||||||
throw Error('Response from SinusBot does not have any data to parse.');
|
throw Error('Response from SinusBot does not have any data to parse.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return (
|
||||||
// TODO: Remove hardcoded username filter for bots.
|
response
|
||||||
.filter((user: TunakillUser) => user.name !== "Server Query Admin" && user.name !== "DJ Inshalla")
|
// TODO: Remove hardcoded username filter for bots.
|
||||||
|
.filter(
|
||||||
|
(user: TunakillUser) =>
|
||||||
|
user.name !== 'Server Query Admin' && user.name !== 'DJ Inshalla',
|
||||||
|
)
|
||||||
|
|
||||||
.filter((user: TunakillUser) => user.time != null)
|
.filter((user: TunakillUser) => user.time != null)
|
||||||
.map((user: TunakillUser) => {
|
.map((user: TunakillUser) => {
|
||||||
return {
|
return {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
rawTime: user.time,
|
rawTime: user.time,
|
||||||
onlineTime: this.humanizeTime(user.time)
|
onlineTime: this.humanizeTime(user.time),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a: any, b: any) => this.sortByDescendingTime(a.rawTime, b.rawTime));
|
.sort((a: any, b: any) =>
|
||||||
|
this.sortByDescendingTime(a.rawTime, b.rawTime),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
private requestConfig = (body?: string, bearerToken?: string, requestType: string = 'POST'): RequestInit => {
|
private requestConfig = (
|
||||||
|
body?: string,
|
||||||
|
bearerToken?: string,
|
||||||
|
requestType: string = 'POST',
|
||||||
|
): RequestInit => {
|
||||||
return {
|
return {
|
||||||
method: requestType,
|
method: requestType,
|
||||||
body: body,
|
body: body,
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': '*/*',
|
Accept: '*/*',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'HumeniusTSRankingBackend/0.0.2',
|
'User-Agent': 'HumeniusTSRankingBackend/0.0.2',
|
||||||
'Authorization': `Bearer ${bearerToken}`
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
private createHttpException = (statusCode: HttpStatus, message?: string): HttpException => {
|
private createHttpException = (
|
||||||
return new HttpException({
|
statusCode: HttpStatus,
|
||||||
status: statusCode,
|
message?: string,
|
||||||
error: message
|
): HttpException => {
|
||||||
}, statusCode);
|
return new HttpException(
|
||||||
}
|
{
|
||||||
|
status: statusCode,
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
statusCode,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private checkStatus = response => {
|
private checkStatus = response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -151,7 +181,7 @@ export class SinusBotService {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
};
|
||||||
|
|
||||||
private sortByDescendingTime = (a: number, b: number) => {
|
private sortByDescendingTime = (a: number, b: number) => {
|
||||||
if (a < b) {
|
if (a < b) {
|
||||||
@@ -163,16 +193,16 @@ export class SinusBotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
private humanizeTime = (seconds: number) => {
|
private humanizeTime = (seconds: number) => {
|
||||||
const d = Math.floor(seconds / (3600 * 24));
|
const d = Math.floor(seconds / (3600 * 24));
|
||||||
const h = Math.floor(seconds % (3600 * 24) / 3600);
|
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
const m = Math.floor(seconds % 3600 / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
|
|
||||||
return `${d}d ${h}h ${m}m ${s}s`;
|
return `${d}d ${h}h ${m}m ${s}s`;
|
||||||
}
|
};
|
||||||
|
|
||||||
private static logResponse(res: any) {
|
private static logResponse(res: any) {
|
||||||
logger.debug(res);
|
logger.debug(res);
|
||||||
|
|||||||
@@ -4,41 +4,92 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
image: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend:latest
|
image: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend:latest
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- traefik-proxy
|
||||||
env_file: .env
|
env_file: .env
|
||||||
labels:
|
deploy:
|
||||||
- traefik.enable=true
|
resources:
|
||||||
- traefik.http.services.tsotr-frontend.loadbalancer.server.port=5000
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: 1024M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 512M
|
||||||
|
labels:
|
||||||
|
traefik.enable: "true"
|
||||||
|
|
||||||
# HTTP + Redirect
|
traefik.docker.network: traefik-proxy
|
||||||
- traefik.http.routers.tsotr-frontend.entrypoints=web
|
traefik.constraint-label: traefik-proxy
|
||||||
- traefik.http.routers.tsotr-frontend.rule=Host(`tsotr.humenius.me`)
|
|
||||||
- traefik.http.routers.tsotr-frontend.middlewares=redirect@file
|
|
||||||
|
|
||||||
# HTTPS
|
# Watchtower update
|
||||||
- traefik.http.routers.tsotr-frontend-secure.entrypoints=web-secure
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
- traefik.http.routers.tsotr-frontend-secure.rule=Host(`tsotr.humenius.me`)
|
|
||||||
- traefik.http.routers.tsotr-frontend-secure.tls.certresolver=letsencrypt
|
# HTTPS
|
||||||
|
traefik.http.routers.tsotr-frontend-secure.entrypoints: web-secure
|
||||||
|
traefik.http.routers.tsotr-frontend-secure.rule: Host(`tsotr.humenius.me`)
|
||||||
|
traefik.http.routers.tsotr-frontend-secure.tls.certresolver: letsencrypt
|
||||||
|
traefik.http.services.tsotr-frontend.loadbalancer.server.port: 5000
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: docker.humenius.me/humenius/ts-onlinetime-ranks-backend:latest
|
image: docker.humenius.me/humenius/ts-onlinetime-ranks-backend:latest
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- traefik-proxy
|
||||||
|
- ts-onlinetime-ranks
|
||||||
env_file: .env
|
env_file: .env
|
||||||
labels:
|
deploy:
|
||||||
- traefik.enable=true
|
resources:
|
||||||
- traefik.http.services.tsotr-backend.loadbalancer.server.port=3000
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: 1024M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 512M
|
||||||
|
labels:
|
||||||
|
traefik.enable: "true"
|
||||||
|
|
||||||
# HTTP + Redirect
|
traefik.docker.network: traefik-proxy
|
||||||
- traefik.http.routers.tsotr-backend.entrypoints=web
|
traefik.constraint-label: traefik-proxy
|
||||||
- traefik.http.routers.tsotr-backend.rule=Host(`api.tsotr.humenius.me`)
|
|
||||||
- traefik.http.routers.tsotr-backend.middlewares=redirect@file
|
|
||||||
|
|
||||||
# HTTPS
|
# Watchtower update
|
||||||
- traefik.http.routers.tsotr-backend-secure.entrypoints=web-secure
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
- traefik.http.routers.tsotr-backend-secure.rule=Host(`api.tsotr.humenius.me`)
|
|
||||||
- traefik.http.routers.tsotr-backend-secure.tls.certresolver=letsencrypt
|
# HTTPS
|
||||||
|
traefik.http.routers.tsotr-backend-secure.entrypoints: web-secure
|
||||||
|
traefik.http.routers.tsotr-backend-secure.rule: Host(`api.tsotr.humenius.me`)
|
||||||
|
traefik.http.routers.tsotr-backend-secure.tls.certresolver: letsencrypt
|
||||||
|
traefik.http.services.tsotr-backend.loadbalancer.server.port: 3500
|
||||||
|
|
||||||
|
db:
|
||||||
|
hostname: ts-onlinetime-ranks-db
|
||||||
|
image: docker.io/bitnami/mariadb:10.5-debian-10
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- target: 3306
|
||||||
|
published: 13307
|
||||||
|
mode: host
|
||||||
|
networks:
|
||||||
|
- ts-onlinetime-ranks
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: 1024M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 512M
|
||||||
|
volumes:
|
||||||
|
- ts-onlinetime-ranks-db:/bitnami/mariadb
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
traefik-proxy:
|
||||||
external: true
|
external: true
|
||||||
|
ts-onlinetime-ranks:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ts-onlinetime-ranks-db:
|
||||||
|
driver: local
|
||||||
|
|||||||
4
frontend/.babelrc
Normal file
4
frontend/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-react", "@babel/preset-env"],
|
||||||
|
"plugins": ["emotion"]
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
FROM node:14.3.0-alpine as builder
|
FROM node:14.3.0-alpine as builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV PATH /app/node_modules/.bin:$PATH
|
ENV PATH /app/node_modules/.bin:$PATH
|
||||||
COPY ./package.json ./
|
|
||||||
RUN yarn
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN yarn
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
FROM node:14.3.0-alpine
|
FROM node:14.3.0-alpine
|
||||||
|
|||||||
15
frontend/generate-react-cli.json
Normal file
15
frontend/generate-react-cli.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"usesTypeScript": true,
|
||||||
|
"usesCssModule": false,
|
||||||
|
"cssPreprocessor": "scss",
|
||||||
|
"testLibrary": "Testing Library",
|
||||||
|
"component": {
|
||||||
|
"default": {
|
||||||
|
"path": "src/components",
|
||||||
|
"withStyle": true,
|
||||||
|
"withTest": true,
|
||||||
|
"withStory": false,
|
||||||
|
"withLazy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,22 +2,35 @@
|
|||||||
"name": "ts-onlinetime-ranks-frontend",
|
"name": "ts-onlinetime-ranks-frontend",
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "https://api.tsotr.humenius.me",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
"@types/jest": "^24.0.0",
|
"@types/jest": "^24.0.0",
|
||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react": "^16.9.0",
|
"husky": "^4.3.7",
|
||||||
"@types/react-dom": "^16.9.0",
|
"lint-staged": "^10.5.3",
|
||||||
"react": "^16.13.1",
|
"prettier": "^2.2.1",
|
||||||
"react-dom": "^16.13.1",
|
"react": "17.0.0",
|
||||||
|
"react-dom": "17.0.0",
|
||||||
|
"react-router": "^5.2.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "3.4.1",
|
||||||
|
"react-spinners": "^0.10.4",
|
||||||
"sass": "^1.26.5",
|
"sass": "^1.26.5",
|
||||||
"typescript": "~3.7.2"
|
"typescript": "~3.7.2"
|
||||||
},
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"/src/**/*.{js,jsx,ts,tsx,json,css,scss}": [
|
||||||
|
"prettier --single-quote --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"precommit": "lint-staged",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
@@ -37,5 +50,10 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^16.14.2",
|
||||||
|
"@types/react-dom": "^16.9.10",
|
||||||
|
"@types/react-router-dom": "^5.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
background-color: #282c34; }
|
background-color: #282c34;
|
||||||
|
}
|
||||||
|
|
||||||
.first-place {
|
.first-place {
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.App {
|
@mixin background() {
|
||||||
width: auto;
|
width: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -10,43 +10,19 @@
|
|||||||
background-color: #282c34;
|
background-color: #282c34;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blurred {
|
@mixin blurred() {
|
||||||
-webkit-filter: blur(5px);
|
-webkit-filter: blur(10px);
|
||||||
-moz-filter: blur(5px);
|
-moz-filter: blur(10px);
|
||||||
-o-filter: blur(5px);
|
-o-filter: blur(10px);
|
||||||
-ms-filter: blur(5px);
|
-ms-filter: blur(10px);
|
||||||
filter: blur(5px);
|
filter: blur(10px);
|
||||||
}
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
.first-place {
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
font-size: xx-large;
|
-moz-user-select: none; /* Old versions of Firefox */
|
||||||
font-weight: bolder;
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
color: gold;
|
user-select: none; /* Non-prefixed version, currently
|
||||||
}
|
supported by Chrome, Edge, Opera and Firefox */
|
||||||
|
|
||||||
.second-place {
|
|
||||||
font-size: x-large;
|
|
||||||
font-weight: bold;
|
|
||||||
color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
.third-place {
|
|
||||||
font-size: larger;
|
|
||||||
color: saddlebrown;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 4vw;
|
|
||||||
color: #88c9db;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link,
|
a:link,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react';
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
// test('renders learn react link', () => {
|
||||||
const { getByText } = render(<App />);
|
// const { getByText } = render(<App />);
|
||||||
const linkElement = getByText(/learn react/i);
|
// const linkElement = getByText(/learn react/i);
|
||||||
expect(linkElement).toBeInTheDocument();
|
// expect(linkElement).toBeInTheDocument();
|
||||||
});
|
// });
|
||||||
|
|||||||
@@ -1,115 +1,20 @@
|
|||||||
import React from 'react';
|
import React, {FunctionComponent} from 'react';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import UserStatsMockService from "./mock/UserStatsMockService";
|
import {BrowserRouter as Router, Route, Switch, Redirect} from 'react-router-dom';
|
||||||
import UserStatsService from "./services/UserStatsService";
|
|
||||||
import {findDOMNode} from "react-dom";
|
|
||||||
import TableEntry from "./models/TableEntry";
|
|
||||||
|
|
||||||
interface State {
|
import {library} from "@fortawesome/fontawesome-svg-core";
|
||||||
error?: string,
|
import {faArrowCircleLeft, faArrowCircleRight} from "@fortawesome/free-solid-svg-icons";
|
||||||
isLoaded: boolean,
|
import MainPage, {IMainPageProps} from "./pages/MainPage/MainPage";
|
||||||
users?: TableEntry[],
|
|
||||||
mock?: TableEntry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class App extends React.Component {
|
library.add(faArrowCircleLeft, faArrowCircleRight)
|
||||||
|
|
||||||
private apiService: UserStatsService = new UserStatsService();
|
const App: FunctionComponent = () => (
|
||||||
private mockService = new UserStatsMockService();
|
<Router>
|
||||||
|
<Switch>
|
||||||
|
<Route path={'/season/:id?'} component={(props: IMainPageProps) => (<MainPage {...props}/>)} />
|
||||||
|
<Redirect to={'/season'} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
state: State = {
|
export default App;
|
||||||
error: undefined,
|
|
||||||
isLoaded: false,
|
|
||||||
users: undefined,
|
|
||||||
mock: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({isLoaded: false, mock: this.mockService.getStatsWithoutPromise()});
|
|
||||||
// this.mockService.getStats()
|
|
||||||
// .then(data => this.setState({
|
|
||||||
// data: data,
|
|
||||||
// mock: data
|
|
||||||
// }))
|
|
||||||
// .finally(() => {
|
|
||||||
// });
|
|
||||||
this.apiService.getStats()
|
|
||||||
.then(data => this.setState({
|
|
||||||
isLoaded: true,
|
|
||||||
users: data
|
|
||||||
}))
|
|
||||||
.catch(error => {
|
|
||||||
error.response.json()
|
|
||||||
.then((err: any) => this.setState({
|
|
||||||
isLoaded: true,
|
|
||||||
error: err.error
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const element = findDOMNode(this);
|
|
||||||
if (element != null) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createTableEntries(entries: TableEntry[]) {
|
|
||||||
return entries.map((entry, index) => {
|
|
||||||
const placement = index + 1;
|
|
||||||
const placementClassName = placement === 1 ? "first-place"
|
|
||||||
: (placement === 2 ? "second-place"
|
|
||||||
: (placement === 3 ? "third-place"
|
|
||||||
: undefined))
|
|
||||||
return (
|
|
||||||
<tr key={index} className={placementClassName}>
|
|
||||||
<td>{placement}</td>
|
|
||||||
<td>{entry.name}</td>
|
|
||||||
<td>{entry.onlineTime}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTableData() {
|
|
||||||
const { error, isLoaded, users, mock } = this.state;
|
|
||||||
if (users != null && isLoaded && error == null) {
|
|
||||||
return this.createTableEntries(users);
|
|
||||||
} else if (isLoaded && error != null && mock != null) {
|
|
||||||
return this.createTableEntries(mock);
|
|
||||||
} else if (mock != null) {
|
|
||||||
return this.createTableEntries(mock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<p className="title">Humenius' TeamSpeak 3-Ranking</p>
|
|
||||||
{ this.state.error != null ? <p className="error-message"> { this.state.error } Please try again later!</p> : null}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Placement</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Online time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className={this.state.error != null || !this.state.isLoaded ? "blurred" : undefined}>
|
|
||||||
{this.renderTableData()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<footer>
|
|
||||||
Made by <a href="https://humenius.me"
|
|
||||||
>Humenius</a>.
|
|
||||||
Powered by <a href="https://reactjs.org/"
|
|
||||||
>React</a>.
|
|
||||||
<br/>
|
|
||||||
<a href="ts3server://ts.humenius.me">
|
|
||||||
Click here to join my TeamSpeak server
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
frontend/src/components/ErrorContainer/ErrorContainer.scss
Normal file
11
frontend/src/components/ErrorContainer/ErrorContainer.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.ErrorContainer {
|
||||||
|
color: #f33c3c;
|
||||||
|
font-size: 32px;
|
||||||
|
//box-shadow:
|
||||||
|
// 0 2.8px 2.2px rgba(0, 0, 0, 0.034),
|
||||||
|
// 0 6.7px 5.3px rgba(0, 0, 0, 0.048),
|
||||||
|
// 0 12.5px 10px rgba(0, 0, 0, 0.06),
|
||||||
|
// 0 22.3px 17.9px rgba(0, 0, 0, 0.072),
|
||||||
|
// 0 41.8px 33.4px rgba(0, 0, 0, 0.086),
|
||||||
|
// 0 100px 80px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
14
frontend/src/components/ErrorContainer/ErrorContainer.tsx
Normal file
14
frontend/src/components/ErrorContainer/ErrorContainer.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './ErrorContainer.scss';
|
||||||
|
|
||||||
|
const ErrorContainer: React.FC<IErrorContainerProps> = (props: IErrorContainerProps) => (
|
||||||
|
<div className="ErrorContainer" data-testid="ErrorContainer">
|
||||||
|
{props.message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface IErrorContainerProps {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorContainer;
|
||||||
5
frontend/src/components/Footer/Footer.scss
Normal file
5
frontend/src/components/Footer/Footer.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.Footer {
|
||||||
|
footer {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/src/components/Footer/Footer.tsx
Normal file
17
frontend/src/components/Footer/Footer.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Footer.scss';
|
||||||
|
|
||||||
|
const Footer: React.FC = () => (
|
||||||
|
<div className="Footer" data-testid="Footer">
|
||||||
|
<footer>
|
||||||
|
Made by <a href="https://humenius.me">Humenius</a>.
|
||||||
|
Powered by <a href="https://reactjs.org/">React</a>.
|
||||||
|
<br/>
|
||||||
|
<a href="ts3server://ts.humenius.me">
|
||||||
|
Click here to join my TeamSpeak server
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
7
frontend/src/components/Header/Header.scss
Normal file
7
frontend/src/components/Header/Header.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
.Header {
|
||||||
|
.title {
|
||||||
|
font-size: 4vw;
|
||||||
|
color: #88c9db;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/components/Header/Header.tsx
Normal file
10
frontend/src/components/Header/Header.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Header.scss';
|
||||||
|
|
||||||
|
const Header: React.FC = () => (
|
||||||
|
<div className="Header" data-testid="Header">
|
||||||
|
<p className="title">Humenius' TeamSpeak 3-Ranking</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Header;
|
||||||
4
frontend/src/components/SeasonDetail/SeasonDetail.scss
Normal file
4
frontend/src/components/SeasonDetail/SeasonDetail.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.SeasonDetail {
|
||||||
|
vertical-align: middle;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
28
frontend/src/components/SeasonDetail/SeasonDetail.tsx
Normal file
28
frontend/src/components/SeasonDetail/SeasonDetail.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './SeasonDetail.scss';
|
||||||
|
import {IUserListProperties} from "../UserList/UserList";
|
||||||
|
|
||||||
|
const SeasonDetail: React.FC<IUserListProperties> = (props: IUserListProperties) => {
|
||||||
|
const { seasonId, dates } = props.userStats
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="SeasonDetail" data-testid="SeasonDetail">
|
||||||
|
Season {seasonId} - Duration: {dateToString(dates.start)} - {dateToString(dates.end)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateToString = (date?: Date): string => {
|
||||||
|
return date ? date.toLocaleDateString('en-GB') : 'TBD';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonDetailProperties {
|
||||||
|
seasonId?: string,
|
||||||
|
maxSeasonId: string
|
||||||
|
dates: {
|
||||||
|
start?: Date,
|
||||||
|
end?: Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeasonDetail;
|
||||||
17
frontend/src/components/SeasonSwitch/SeasonSwitch.scss
Normal file
17
frontend/src/components/SeasonSwitch/SeasonSwitch.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.SeasonSwitch {
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 3vw;
|
||||||
|
max-font-size: 4vw;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-click {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
frontend/src/components/SeasonSwitch/SeasonSwitch.tsx
Normal file
45
frontend/src/components/SeasonSwitch/SeasonSwitch.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
import './SeasonSwitch.scss';
|
||||||
|
import SeasonDetail from '../SeasonDetail/SeasonDetail';
|
||||||
|
import {IUserListProperties} from "../UserList/UserList";
|
||||||
|
|
||||||
|
const SeasonSwitch: React.FC<IUserListProperties> = (props: IUserListProperties) => {
|
||||||
|
const seasonId = Number(props.userStats.seasonId)
|
||||||
|
const maxSeasonId = Number(props.userStats.maxSeasonId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"SeasonSwitch"} data-testid="SeasonSwitch">
|
||||||
|
{
|
||||||
|
seasonId > 1
|
||||||
|
&& (props.enabled
|
||||||
|
&& <Link to={"/season/" + (seasonId - 1)} onClick={() => {
|
||||||
|
console.log("nothing happens")
|
||||||
|
props.onSeasonIdChange('' + (seasonId - 1))}}>
|
||||||
|
<FontAwesomeIcon icon="arrow-circle-left"/>
|
||||||
|
</Link>
|
||||||
|
|| <Link to={""} onClick={(e) => {
|
||||||
|
console.log("nothing happens2")
|
||||||
|
e.preventDefault()}}>
|
||||||
|
<FontAwesomeIcon icon="arrow-circle-left"/>
|
||||||
|
</Link>)
|
||||||
|
}
|
||||||
|
|
||||||
|
<SeasonDetail {...props} />
|
||||||
|
|
||||||
|
{
|
||||||
|
seasonId < maxSeasonId
|
||||||
|
&& (props.enabled
|
||||||
|
? <Link to={"/season/" + (seasonId + 1)} onClick={() => props.onSeasonIdChange('' + (seasonId + 1))}>
|
||||||
|
<FontAwesomeIcon icon="arrow-circle-right"/>
|
||||||
|
</Link>
|
||||||
|
: <Link to={""} onClick={(e) => e.preventDefault()}>
|
||||||
|
<FontAwesomeIcon icon="arrow-circle-right"/>
|
||||||
|
</Link>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeasonSwitch;
|
||||||
23
frontend/src/components/UserList/UserList.scss
Normal file
23
frontend/src/components/UserList/UserList.scss
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.UserList {
|
||||||
|
.first-place {
|
||||||
|
font-size: xx-large;
|
||||||
|
font-weight: bolder;
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-place {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: bold;
|
||||||
|
color: silver;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-place {
|
||||||
|
font-size: larger;
|
||||||
|
color: saddlebrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SeasonSwitch {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
50
frontend/src/components/UserList/UserList.tsx
Normal file
50
frontend/src/components/UserList/UserList.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableEntry from '../../models/TableEntry';
|
||||||
|
import './UserList.scss';
|
||||||
|
import SeasonSwitch from "../SeasonSwitch/SeasonSwitch";
|
||||||
|
import UserStatsResponse from "../../models/UserStatsResponse";
|
||||||
|
|
||||||
|
const createTableEntries = (entries: TableEntry[]) =>
|
||||||
|
entries.map((entry, index) => {
|
||||||
|
const placement = index + 1;
|
||||||
|
const placementClassName = placement === 1 ? "first-place"
|
||||||
|
: (placement === 2 ? "second-place"
|
||||||
|
: (placement === 3 ? "third-place"
|
||||||
|
: undefined))
|
||||||
|
return (
|
||||||
|
<tr key={index} className={placementClassName}>
|
||||||
|
<td>{placement}</td>
|
||||||
|
<td>{entry.name}</td>
|
||||||
|
<td>{entry.rank}</td>
|
||||||
|
<td>{entry.onlineTime}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserList: React.FC<IUserListProperties> = (props: IUserListProperties) => (
|
||||||
|
<div className="UserList" data-testid="UserList">
|
||||||
|
<SeasonSwitch {...props} />
|
||||||
|
<table className={!props.enabled ? "blurred" : ""}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Placement</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Online time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{createTableEntries(props.userStats.stats)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface IUserListProperties {
|
||||||
|
userStats: UserStatsResponse
|
||||||
|
mocked?: boolean
|
||||||
|
onSeasonIdChange: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserList;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
|
||||||
import * as serviceWorker from './serviceWorker';
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
import TableEntry from "../models/TableEntry";
|
import UserStatsResponse from "../models/UserStatsResponse";
|
||||||
import UserStatsService from "../services/UserStatsService";
|
import UserStatsService from "../services/UserStatsService";
|
||||||
|
|
||||||
export default class UserStatsMockService extends UserStatsService {
|
export default class UserStatsMockService extends UserStatsService {
|
||||||
private readonly entries: TableEntry[];
|
private static readonly mocks: UserStatsResponse[] = [
|
||||||
|
{
|
||||||
|
seasonId: "1",
|
||||||
|
maxSeasonId: "2",
|
||||||
|
dates: {
|
||||||
|
start: new Date(2019, 12, 5),
|
||||||
|
end: new Date()
|
||||||
|
},
|
||||||
|
stats: [
|
||||||
|
{ name: "Humen", rank: "Overwatch Noob 2", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Random Rank 3 4", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Kas is cool 5", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Bremsspu 6r", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "127 3", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "ok343", onlineTime: "0d 1h 0m 0s" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seasonId: "2",
|
||||||
|
maxSeasonId: "2",
|
||||||
|
dates: {
|
||||||
|
start: new Date(new Date().getFullYear(), 0, 1),
|
||||||
|
end: new Date()
|
||||||
|
},
|
||||||
|
stats: [
|
||||||
|
{ name: "Humen", rank: "Overwatch Noob", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Random Rank 3", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Kas is cool", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Bremsspur", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "12", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "ok", onlineTime: "0d 1h 0m 0s" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seasonId: 'undefined',
|
||||||
|
maxSeasonId: "2",
|
||||||
|
dates: {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined
|
||||||
|
},
|
||||||
|
stats: [
|
||||||
|
{ name: "Humen", rank: "Overwatch Noob", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Random Rank 3", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Kas is cool", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "Bremsspur", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "12", onlineTime: "0d 1h 0m 0s" },
|
||||||
|
{ name: "Humen", rank: "ok", onlineTime: "0d 1h 0m 0s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
constructor() {
|
public static async getStats(seasonId?: string): Promise<UserStatsResponse> {
|
||||||
super();
|
return Promise.resolve(this.mocks[Number((seasonId ? seasonId : 2)) - 1])
|
||||||
this.entries = [
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" },
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" },
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" },
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" },
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" },
|
|
||||||
{ name: "Humen", onlineTime: "0d 1h 0m 0s" }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStats(): Promise<TableEntry[]> {
|
static getStatsWithoutPromise(seasonId?: string): UserStatsResponse {
|
||||||
return Promise.resolve(this.entries);
|
return this.mocks[Number((seasonId ? seasonId : 2)) - 1]
|
||||||
}
|
|
||||||
|
|
||||||
getStatsWithoutPromise(): TableEntry[] {
|
|
||||||
return this.entries;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
export default class RequestError extends Error {
|
export default class RequestError extends Error {
|
||||||
response?: any;
|
//response?: any = "Unknown error. Please try again later.";
|
||||||
|
statusCode: number
|
||||||
|
message: string
|
||||||
|
|
||||||
|
|
||||||
|
constructor(statusCode: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export default interface TableEntry {
|
export default interface TableEntry {
|
||||||
name: string;
|
name: string;
|
||||||
|
rank: string;
|
||||||
onlineTime: string;
|
onlineTime: string;
|
||||||
}
|
}
|
||||||
|
|||||||
6
frontend/src/models/UserStatsResponse.tsx
Normal file
6
frontend/src/models/UserStatsResponse.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import TableEntry from "./TableEntry";
|
||||||
|
import {SeasonDetailProperties} from "../components/SeasonDetail/SeasonDetail";
|
||||||
|
|
||||||
|
export default interface UserStatsResponse extends SeasonDetailProperties {
|
||||||
|
stats: TableEntry[]
|
||||||
|
}
|
||||||
14
frontend/src/pages/MainPage/MainPage.scss
Normal file
14
frontend/src/pages/MainPage/MainPage.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@import "src/App.scss";
|
||||||
|
|
||||||
|
.MainPage {
|
||||||
|
@include background();
|
||||||
|
|
||||||
|
.blurred {
|
||||||
|
@include blurred();
|
||||||
|
}
|
||||||
|
.ErrorContainer {
|
||||||
|
margin: 0;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
frontend/src/pages/MainPage/MainPage.tsx
Normal file
62
frontend/src/pages/MainPage/MainPage.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, {FC, useEffect, useState} from "react";
|
||||||
|
import UserStatsMockService from "../../mock/UserStatsMockService";
|
||||||
|
import Header from "../../components/Header/Header";
|
||||||
|
import UserList from "../../components/UserList/UserList";
|
||||||
|
import ErrorContainer from "../../components/ErrorContainer/ErrorContainer";
|
||||||
|
import Footer from "../../components/Footer/Footer";
|
||||||
|
import {RouteComponentProps} from "react-router/index";
|
||||||
|
import UserStatsResponse from "../../models/UserStatsResponse";
|
||||||
|
import {withRouter} from "react-router";
|
||||||
|
import RequestError from "../../models/RequestError";
|
||||||
|
import './MainPage.scss';
|
||||||
|
import {ClipLoader} from "react-spinners";
|
||||||
|
import UserStatsService from "../../services/UserStatsService";
|
||||||
|
|
||||||
|
const MainPage: FC<IMainPageProps> = (props: IMainPageProps) => {
|
||||||
|
const [seasonId, setSeasonId] = useState(props.match.params.id)
|
||||||
|
const [error, setError] = useState<RequestError | undefined>(undefined)
|
||||||
|
const [loading, setLoadingState] = useState(true)
|
||||||
|
const [seasonStats, setSeasonStats] = useState<UserStatsResponse>(UserStatsMockService.getStatsWithoutPromise(seasonId))
|
||||||
|
const [spinnerColor] = useState('#61dafb')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoadingState(true)
|
||||||
|
setSeasonStats(UserStatsMockService.getStatsWithoutPromise('3'))
|
||||||
|
UserStatsService.getStats(seasonId)
|
||||||
|
.then(res => {
|
||||||
|
setSeasonStats(res)
|
||||||
|
setLoadingState(false)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err.message)
|
||||||
|
setLoadingState(false)
|
||||||
|
setError(new RequestError(0, "Could not retrieve stats. Try again later."))
|
||||||
|
})
|
||||||
|
}, [seasonId])
|
||||||
|
|
||||||
|
const spinnerCss = `
|
||||||
|
margin: 0;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="MainPage">
|
||||||
|
<Header />
|
||||||
|
{
|
||||||
|
error && <ErrorContainer message={error.message} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<ClipLoader color={spinnerColor} loading={loading} size={150} css={spinnerCss} />
|
||||||
|
<UserList enabled={!loading && (error == null)} onSeasonIdChange={setSeasonId} userStats={seasonStats} />
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMainPageProps extends RouteComponentProps<{ id?: string }> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(MainPage)
|
||||||
@@ -1,31 +1,34 @@
|
|||||||
import TableEntry from "../models/TableEntry";
|
|
||||||
import RequestError from "../models/RequestError";
|
import RequestError from "../models/RequestError";
|
||||||
|
import UserStatsResponse from "../models/UserStatsResponse";
|
||||||
|
|
||||||
export default class UserStatsService {
|
export default class UserStatsService {
|
||||||
|
|
||||||
private apiURL = 'https://api.tsotr.humenius.me/stats'
|
private static apiURL = 'https://api.tsotr.humenius.me/stats'
|
||||||
|
//private static apiURL = 'http://localhost:3500/stats'
|
||||||
|
|
||||||
private requestInit: RequestInit = {
|
private static requestInit: RequestInit = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async getStats(): Promise<TableEntry[]> {
|
public static async getStats(seasonId?: string): Promise<UserStatsResponse> {
|
||||||
return fetch(this.apiURL, this.requestInit)
|
return fetch(`${this.apiURL}/season/${seasonId ?? ''}`, this.requestInit)
|
||||||
.then(res => UserStatsService.checkResponse(res))
|
.then(res => UserStatsService.checkResponse(res))
|
||||||
.then(data => data.json());
|
.then(data => data.json())
|
||||||
|
.then(data => {
|
||||||
|
data.dates.start = data.dates.start ? new Date(data.dates.start) : undefined
|
||||||
|
data.dates.end = data.dates.end ? new Date(data.dates.end) : undefined
|
||||||
|
return data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private static checkResponse(response: any): any {
|
private static checkResponse(response: any): any {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
let error = new RequestError(response.statusText);
|
throw new RequestError(response.status, response.body);
|
||||||
error.response = response;
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3909
frontend/yarn.lock
3909
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
48
stack.beta.yml
Normal file
48
stack.beta.yml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
image: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend:dev-latest
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
env_file: .env_beta
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.services.tsotr-frontend.loadbalancer.server.port=5000
|
||||||
|
|
||||||
|
# Watchtower update
|
||||||
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
|
|
||||||
|
# HTTPS
|
||||||
|
- traefik.http.routers.tsotr-frontend-secure.entrypoints=web-secure
|
||||||
|
- traefik.http.routers.tsotr-frontend-secure.rule=Host(`beta.tsotr.humenius.me`)
|
||||||
|
- traefik.http.routers.tsotr-frontend-secure.tls.certresolver=letsencrypt
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: docker.humenius.me/humenius/ts-onlinetime-ranks-backend:dev-latest
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- ts-onlinetime-ranks
|
||||||
|
env_file: .env_beta
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.services.tsotr-backend.loadbalancer.server.port=3500
|
||||||
|
|
||||||
|
# Watchtower update
|
||||||
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
|
|
||||||
|
# HTTPS
|
||||||
|
- traefik.http.routers.tsotr-backend-secure.entrypoints=web-secure
|
||||||
|
- traefik.http.routers.tsotr-backend-secure.rule=Host(`api.beta.tsotr.humenius.me`)
|
||||||
|
- traefik.http.routers.tsotr-backend-secure.tls.certresolver=letsencrypt
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
ts-onlinetime-ranks:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ts-onlinetime-ranks-db:
|
||||||
|
driver: local
|
||||||
|
|
||||||
Reference in New Issue
Block a user