diff --git a/.env.example b/.env.example index 821e903..f46c8c7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ POSTGRES_PORT=5432 EXPRESS_PORT=3000 +FRONTEND_PORT=8080 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres diff --git a/README.md b/README.md index 71ff3b6..82d8404 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,30 @@ # RaspberryRocketeer -## Class Diagram of all classes +## How to run -```mermaid -classDiagram -direction BT -class Collidable { - collides(o: Entity) boolean -} -class Database { - any _db - any db -} -class Entity { - constructor(position: Position, width: number, height: number, fill: number) - Position _position - number _width - number _height - number fill - boolean _showHitbox - update() void - draw() void - Position position - number width - number height - boolean showHitbox -} -class Game { - number id - number score - string playtime - Date date - number userId -} -class GamePgPromiseRepository { - insert(game: Game) Promise~Game~ - serialize(raw: any) Game -} -class GameRepository { - insert(game: Game) Promise~Game~ -} -class HighscoreLeaderboard -class HighscoreLeaderboardPgPromiseRepository { - getAll() Promise~HighscoreLeaderboard~ - serialize(raw: any) HighscoreLeaderboard -} -class HighscoreLeaderboardRepository { - getAll() Promise~HighscoreLeaderboard~ -} -class Leaderboard -class LeaderboardEntry { - number username - number rank - T score -} -class Obstacle { - constructor(position: Position, obstacleWidth: number, obstacleHeight: number, pipeImagePath: string) - Pipe pipeTop - Pipe pipeBottom - number padding - number speed - number _distanceBetweenPipes - number _startX - resetPosition() void - randomizeHeight() void - randomRange(min: number, max: number) number - update() void - draw() void - collides(o: Entity) boolean - any startX - any distanceBetweenPipes -} -class Pipe { - constructor(positionX: number, width: number, height: number) - any _image - update() void - draw() void - move(speed: number) void - collides(o: Entity) boolean - any image -} -class Position { - constructor(x: number, y: number) - number _x - number _y - number x - number y -} -class Raspberry { - constructor(image: string) - number lift - number gravity - number _velocity - any _image - Position position - number maxVelocity - number WIDTH - number HEIGHT - number FILL - update() void - applyGravity() void - forceBoundaries() void - boost() void - draw() void - number velocity - any image -} -class TimeLeaderboard -class TimeLeaderboardPgPromiseRepository { - getAll() Promise~TimeLeaderboard~ - serialize(raw: any) TimeLeaderboard -} -class TimeLeaderboardRepository { - getAll() Promise~TimeLeaderboard~ -} -class User { - number id - string name -} -class UserPgPromiseRepository { - getById(id: number) Promise~User~ - getByName(name: string) Promise~User~ - withIdExists(id: number) Promise~boolean~ - withNameExists(name: string) Promise~boolean~ - insert(user: Omit~User, "id"~) Promise~User~ - serialize(raw: any) User -} -class UserRepository { - getById(id: number) Promise~User~ - getByName(name: string) Promise~User~ - withIdExists(userId: number) Promise~boolean~ - withNameExists(username: string) Promise~boolean~ - insert(user: Omit~User, "id"~) Promise~User~ -} -class UserScores { - number userId - number highscore - number totalScore - string totalPlaytime - number averageScore - number gamesPlayed -} -class UserScoresPgPromiseRepository { - getById(id: number) Promise~UserScores~ - serialize(raw: any) UserScores -} -class UserScoresRepository { - getById(userId: number) Promise~UserScores~ -} - -GamePgPromiseRepository --> GameRepository -HighscoreLeaderboardPgPromiseRepository --> HighscoreLeaderboardRepository -Obstacle ..> Collidable -Obstacle --> Entity -Pipe ..> Collidable -Pipe --> Entity -Raspberry --> Entity -TimeLeaderboardPgPromiseRepository --> TimeLeaderboardRepository -UserPgPromiseRepository --> UserRepository -UserScoresPgPromiseRepository --> UserScoresRepository +### Copy .env +First you need to copy the `.env.example`. +```shell +cp .env.example .env ``` + +Note: It is recommended to change the values for the database user. + +### Install node packages +Go into the frontend folder using +```shell +cd frontend +``` +and run: +```shell +npm install +``` + +### Start the container (in the project root) + +```shell +docker compose up --build +``` + +You can then access the website on `localhost:8080` diff --git a/backend/api/src/gameRoute.ts b/backend/api/src/gameRoute.ts index 2ac9175..0de3e2c 100644 --- a/backend/api/src/gameRoute.ts +++ b/backend/api/src/gameRoute.ts @@ -9,6 +9,9 @@ export const gameRoute = express.Router() gameRoute.use(express.json()) +/** + * Test + */ gameRoute.post( '/add', body('playtime') @@ -18,6 +21,8 @@ gameRoute.post( body('userId') .isInt({min: 1}) .custom(userWithIdExists), + body('score') + .isInt({min: 0}), /** * After processing the errors of express-validator, inserts the game into the DB * @param req diff --git a/backend/api/src/leaderboardRoute.ts b/backend/api/src/leaderboardRoute.ts index e77fb2e..699180c 100644 --- a/backend/api/src/leaderboardRoute.ts +++ b/backend/api/src/leaderboardRoute.ts @@ -1,4 +1,5 @@ import express from 'express'; +import {query, validationResult} from 'express-validator'; import {TimeLeaderboardRepository} from "./repositories/TimeLeaderboardRepository.js"; import {TimeLeaderboardPgPromiseRepository} from "./repositories/pgPromise/TimeLeaderboardPgPromiseRepository.js"; import {HighscoreLeaderboardPgPromiseRepository} from "./repositories/pgPromise/HighscoreLeaderboardPgPromiseRepository.js"; @@ -8,6 +9,9 @@ import {HighscoreLeaderboard, TimeLeaderboard} from "./model/Leaderboard.js"; export const leaderboardRoute = express.Router() leaderboardRoute.get('/highscore', + query('pagination').toBoolean(), + query('entriesPerPage').optional().isInt({min: 1}).toInt(), + query('page').optional().isInt({min: 0}).toInt(), /** * Returns the highscore leaderboard as JSON response, fetched from DB * @param req @@ -15,8 +19,21 @@ leaderboardRoute.get('/highscore', */ async (req, res) => { try { + //region validate parameters + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + //endregion const highscoreLeaderboardRepo: HighscoreLeaderboardRepository = new HighscoreLeaderboardPgPromiseRepository; - const highscoreLeaderboard: HighscoreLeaderboard = await highscoreLeaderboardRepo.getAll(); + let highscoreLeaderboard: HighscoreLeaderboard; + if (req.query.pagination == true) { + const entriesPerPage = req.query.entriesPerPage; + const page = req.query.page; + highscoreLeaderboard = await highscoreLeaderboardRepo.getPage(entriesPerPage, page); + } else { + highscoreLeaderboard = await highscoreLeaderboardRepo.getAll(); + } res.send(highscoreLeaderboard); } catch (error) { // handle errors @@ -26,6 +43,9 @@ leaderboardRoute.get('/highscore', }) leaderboardRoute.get('/totalplaytime', + query('pagination').toBoolean(), + query('entriesPerPage').optional().isInt({min: 1}).toInt(), + query('page').optional().isInt({min: 0}).toInt(), /** * Returns the total playtime leaderboard as JSON response, fetched from DB * @param req @@ -33,8 +53,23 @@ leaderboardRoute.get('/totalplaytime', */ async (req, res) => { try { + console.log(req.query) + //region validate parameters + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + //endregion const timeLeaderboardRepo: TimeLeaderboardRepository = new TimeLeaderboardPgPromiseRepository; - const timeLeaderboard: TimeLeaderboard = await timeLeaderboardRepo.getAll(); + let timeLeaderboard: TimeLeaderboard; + console.log(req.query) + if (req.query.pagination == true) { + const entriesPerPage = req.query.entriesPerPage; + const page = req.query.page; + timeLeaderboard = await timeLeaderboardRepo.getPage(entriesPerPage, page); + } else { + timeLeaderboard = await timeLeaderboardRepo.getAll(); + } res.send(timeLeaderboard); } catch (error) { // handle errors diff --git a/backend/api/src/repositories/HighscoreLeaderboardRepository.ts b/backend/api/src/repositories/HighscoreLeaderboardRepository.ts index 204feff..d70d3d5 100644 --- a/backend/api/src/repositories/HighscoreLeaderboardRepository.ts +++ b/backend/api/src/repositories/HighscoreLeaderboardRepository.ts @@ -2,4 +2,5 @@ import {HighscoreLeaderboard} from "../model/Leaderboard.js"; export abstract class HighscoreLeaderboardRepository { abstract getAll(): Promise; + abstract getPage(entriesPerPage: number, page: number): Promise } \ No newline at end of file diff --git a/backend/api/src/repositories/TimeLeaderboardRepository.ts b/backend/api/src/repositories/TimeLeaderboardRepository.ts index ef9d8fe..8c02717 100644 --- a/backend/api/src/repositories/TimeLeaderboardRepository.ts +++ b/backend/api/src/repositories/TimeLeaderboardRepository.ts @@ -2,4 +2,5 @@ import {TimeLeaderboard} from "../model/Leaderboard.js"; export abstract class TimeLeaderboardRepository { abstract getAll(): Promise; + abstract getPage(entriesPerPage: number, page: number); } \ No newline at end of file diff --git a/backend/api/src/repositories/pgPromise/HighscoreLeaderboardPgPromiseRepository.ts b/backend/api/src/repositories/pgPromise/HighscoreLeaderboardPgPromiseRepository.ts index 8a7747c..36c9043 100644 --- a/backend/api/src/repositories/pgPromise/HighscoreLeaderboardPgPromiseRepository.ts +++ b/backend/api/src/repositories/pgPromise/HighscoreLeaderboardPgPromiseRepository.ts @@ -5,7 +5,15 @@ import {Database} from "../../Database.js"; export class HighscoreLeaderboardPgPromiseRepository extends HighscoreLeaderboardRepository { async getAll(): Promise { const raw: any = await Database.db.manyOrNone( - 'SELECT * FROM lb_highscore INNER JOIN "user" ON user_id = id ORDER BY RANK;' + 'SELECT * FROM lb_highscore INNER JOIN "user" ON user_id = id ORDER BY rank;' + ); + return this.serialize(raw); + } + + async getPage(entriesPerPage, page): Promise { + const raw: any = await Database.db.manyOrNone( + 'SELECT * FROM lb_highscore INNER JOIN "user" ON user_id = id ORDER BY rank LIMIT $1 OFFSET $2;', + [entriesPerPage, page * entriesPerPage] ); return this.serialize(raw); } diff --git a/backend/api/src/repositories/pgPromise/TimeLeaderboardPgPromiseRepository.ts b/backend/api/src/repositories/pgPromise/TimeLeaderboardPgPromiseRepository.ts index 1345562..b2b2ca8 100644 --- a/backend/api/src/repositories/pgPromise/TimeLeaderboardPgPromiseRepository.ts +++ b/backend/api/src/repositories/pgPromise/TimeLeaderboardPgPromiseRepository.ts @@ -5,7 +5,15 @@ import {Database} from "../../Database.js"; export class TimeLeaderboardPgPromiseRepository extends TimeLeaderboardRepository { async getAll(): Promise { const raw: any = await Database.db.manyOrNone( - 'SELECT * FROM lb_total_playtime INNER JOIN "user" ON user_id = id ORDER BY RANK;' + 'SELECT * FROM lb_total_playtime INNER JOIN "user" ON user_id = id ORDER BY rank;' + ); + return this.serialize(raw); + } + + async getPage(entriesPerPage: number, page: number): Promise { + const raw: any = await Database.db.manyOrNone( + 'SELECT * FROM lb_total_playtime INNER JOIN "user" ON user_id = id ORDER BY rank LIMIT $1 OFFSET $2;', + [entriesPerPage, page * entriesPerPage] ); return this.serialize(raw); } diff --git a/backend/api/src/userRoute.ts b/backend/api/src/userRoute.ts index e87056f..4061969 100644 --- a/backend/api/src/userRoute.ts +++ b/backend/api/src/userRoute.ts @@ -84,3 +84,30 @@ userRoute.get('/:userId/scores', } } ) + +userRoute.get('/:name', + param('name') + .isString() + .isLength({min: 3, max: 32}) + .matches(USERNAME_VALIDATION_REGEX), + + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const name: string = req.params.name; + console.log(name) + + try { + // get & return data + const userRepo: UserPgPromiseRepository = new UserPgPromiseRepository(); + const user = await userRepo.getByName(name); + res.json(user); + } catch (error) { + // handle errors + console.log(error) + res.status(500).json({ errors: [{msg: "Internal server error"}]}) + } + }) diff --git a/backend/db/initScripts/loadMockData.sql b/backend/db/initScripts/loadMockData.sql deleted file mode 100644 index 7735cc9..0000000 --- a/backend/db/initScripts/loadMockData.sql +++ /dev/null @@ -1,40 +0,0 @@ -insert into "user" (name) values ('dpettus0'); -insert into "user" (name) values ('egreetland1'); -insert into "user" (name) values ('smontford2'); -insert into "user" (name) values ('idagwell3'); -insert into "user" (name) values ('lgagan4'); -insert into "user" (name) values ('acarmont5'); -insert into "user" (name) values ('kjermyn6'); -insert into "user" (name) values ('dokieran7'); -insert into "user" (name) values ('pdrinkel8'); - -insert into game (user_id, score, playtime, date) values ('1', 74, '19:59', '2022-07-19'); -insert into game (user_id, score, playtime, date) values ('1', 86, '20:32', '2022-11-24'); -insert into game (user_id, score, playtime, date) values ('1', 68, '10:41', '2022-03-24'); -insert into game (user_id, score, playtime, date) values ('2', 39, '5:55', '2022-06-01'); -insert into game (user_id, score, playtime, date) values ('2', 20, '9:23', '2022-03-12'); -insert into game (user_id, score, playtime, date) values ('2', 28, '23:45', '2022-04-01'); -insert into game (user_id, score, playtime, date) values ('2', 44, '18:43', '2022-06-24'); -insert into game (user_id, score, playtime, date) values ('3', 92, '14:54', '2022-11-06'); -insert into game (user_id, score, playtime, date) values ('3', 73, '0:45', '2022-07-26'); -insert into game (user_id, score, playtime, date) values ('3', 27, '2:49', '2022-02-03'); -insert into game (user_id, score, playtime, date) values ('4', 26, '2:32', '2022-07-19'); -insert into game (user_id, score, playtime, date) values ('4', 12, '17:03', '2022-04-25'); -insert into game (user_id, score, playtime, date) values ('4', 6, '8:49', '2021-12-03'); -insert into game (user_id, score, playtime, date) values ('4', 22, '22:27', '2022-03-02'); -insert into game (user_id, score, playtime, date) values ('5', 94, '1:04', '2022-10-19'); -insert into game (user_id, score, playtime, date) values ('5', 2, '2:14', '2022-04-06'); -insert into game (user_id, score, playtime, date) values ('5', 21, '17:18', '2022-06-03'); -insert into game (user_id, score, playtime, date) values ('6', 33, '16:01', '2022-02-02'); -insert into game (user_id, score, playtime, date) values ('6', 27, '7:03', '2022-02-06'); -insert into game (user_id, score, playtime, date) values ('6', 62, '0:45', '2022-11-15'); -insert into game (user_id, score, playtime, date) values ('7', 12, '8:54', '2022-06-29'); -insert into game (user_id, score, playtime, date) values ('7', 63, '16:01', '2022-11-05'); -insert into game (user_id, score, playtime, date) values ('7', 29, '0:46', '2022-10-01'); -insert into game (user_id, score, playtime, date) values ('8', 67, '1:27', '2022-09-29'); -insert into game (user_id, score, playtime, date) values ('8', 84, '10:37', '2021-12-18'); -insert into game (user_id, score, playtime, date) values ('8', 14, '19:14', '2022-01-31'); -insert into game (user_id, score, playtime, date) values ('9', 21, '19:04', '2022-03-08'); -insert into game (user_id, score, playtime, date) values ('9', 46, '2:34', '2022-04-18'); -insert into game (user_id, score, playtime, date) values ('9', 78, '9:33', '2022-09-10'); -insert into game (user_id, score, playtime, date) values ('9', 82, '11:19', '2022-11-29'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f21adcc..2672685 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,32 @@ -version: '3.1' - -services: - db: - build: backend/db - container_name: postgres-db - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - ports: - - "${POSTGRES_PORT}:5432" - volumes: - - ./backend/pgdata:/var/lib/postgresql/data - - api: - build: backend/api - depends_on: - - db - container_name: express-api - ports: - - "${EXPRESS_PORT}:3000" - +version: '3.1' + +services: + db: + build: backend/db + container_name: postgres-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "${POSTGRES_PORT}:5432" + volumes: + - ./backend/pgdata:/var/lib/postgresql/data + + api: + build: backend/api + depends_on: + - db + container_name: express-api + ports: + - "${EXPRESS_PORT}:3000" + + vue: + build: frontend + container_name: frontend + ports: + - "${FRONTEND_PORT}:8080" + + + volumes: + - ./frontend:/app diff --git a/docs/classes/Entity.Entity.html b/docs/classes/Entity.Entity.html index 39432b8..0824cc2 100644 --- a/docs/classes/Entity.Entity.html +++ b/docs/classes/Entity.Entity.html @@ -28,7 +28,7 @@
  • Pipe
  • Raspberry
  • +
  • Defined in Entity.ts:4
  • @@ -88,7 +88,7 @@

    Returns Entity

    +
  • Defined in Entity.ts:105
  • Properties

    @@ -97,35 +97,35 @@

    Height.

    +
  • Defined in Entity.ts:21
  • _position: Position

    Position.

    +
  • Defined in Entity.ts:9
  • _showHitbox: boolean

    Whether the hitbox (rectangular surrounding) is shown, or not.

    +
  • Defined in Entity.ts:33
  • _width: number

    Width.

    +
  • Defined in Entity.ts:15
  • fill: number

    Color.

    +
  • Defined in Entity.ts:27
  • Accessors

    @@ -137,7 +137,7 @@

    Returns number

    +
  • Defined in Entity.ts:70
  • set height(value: number): void
  • Set height.

    @@ -149,7 +149,7 @@
    value: number
  • Returns void

    +
  • Defined in Entity.ts:78
  • set position(value: Position): void
  • Set position.

    @@ -171,7 +171,7 @@
    value: Position
  • Returns void

    +
  • Defined in Entity.ts:48
  • set showHitbox(value: boolean): void
  • Set the hitbox's visibility.

    @@ -193,7 +193,7 @@
    value: boolean
  • Returns void

    +
  • Defined in Entity.ts:93
  • set width(value: number): void
  • Set width.

    @@ -215,7 +215,7 @@
    value: number
  • Returns void

    +
  • Defined in Entity.ts:63
  • Methods

    @@ -227,7 +227,7 @@

    Returns void

    +
  • Defined in Entity.ts:121
  • +
  • Defined in Entity.ts:116
  • Returns void

    +
  • Defined in Obstacle.ts:13
  • Methods

    @@ -275,7 +275,7 @@

    Returns boolean

    +
  • Defined in Obstacle.ts:89
    • @@ -296,7 +296,7 @@
      pipeImagePath: string

    Returns void

    +
  • Defined in Obstacle.ts:41
  • +
  • Defined in Obstacle.ts:80
    • @@ -328,7 +328,7 @@

    Returns number

    +
  • Defined in Obstacle.ts:70
  • +
  • Defined in Obstacle.ts:59
    • @@ -349,7 +349,7 @@ Randomises the height of the pipes using the padding variable

      Returns void

    +
  • Defined in Obstacle.ts:50
    • @@ -360,7 +360,7 @@ Randomises the height of the pipes using the padding variable

      Returns void

    +
  • Defined in Obstacle.ts:74
  • Returns void

    +
  • Defined in Pipe.ts:23
  • set position(value: Position): void
  • Set position.

    @@ -173,7 +173,7 @@

    Returns void

  • +
  • Defined in Entity.ts:48
  • set showHitbox(value: boolean): void
  • Set the hitbox's visibility.

    @@ -197,7 +197,7 @@

    Returns void

  • +
  • Defined in Entity.ts:93
  • set width(value: number): void
  • Set width.

    @@ -221,7 +221,7 @@

    Returns void

  • +
  • Defined in Entity.ts:63
  • Methods

    @@ -241,7 +241,7 @@

    Returns boolean

    +
  • Defined in Pipe.ts:68
  • +
  • Defined in Pipe.ts:48
    • @@ -269,7 +269,7 @@

    Returns void

    +
  • Defined in Pipe.ts:60
  • +
  • Defined in Pipe.ts:43
  • Returns Position

    +
  • Defined in Position.ts:55
  • Properties

    @@ -75,14 +75,14 @@

    X coordinate.

    +
  • Defined in Position.ts:9
  • _y: number

    Y coordinate.

    +
  • Defined in Position.ts:15
  • Accessors

    @@ -94,7 +94,7 @@

    Returns number

    +
  • Defined in Position.ts:22
  • set x(value: number): void
  • Set x.

    @@ -106,7 +106,7 @@
    value: number
  • Returns void

    +
  • Defined in Position.ts:30
  • set y(value: number): void
  • Set y.

    @@ -128,7 +128,7 @@
    value: number
  • Returns void

    +
  • Defined in Position.ts:45
  • Returns void

    +
  • Defined in Raspberry.ts:87
  • set position(value: Position): void
  • Set position.

    @@ -227,7 +227,7 @@

    Returns void

  • +
  • Defined in Entity.ts:48
  • set showHitbox(value: boolean): void
  • Set the hitbox's visibility.

    @@ -251,7 +251,7 @@

    Returns void

  • +
  • Defined in Entity.ts:93
  • set velocity(value: number): void
  • Sets the velocity.

    @@ -273,7 +273,7 @@
    value: number
  • Returns void

    +
  • Defined in Raspberry.ts:72
  • set width(value: number): void
  • Set width.

    @@ -297,7 +297,7 @@

    Returns void

  • +
  • Defined in Entity.ts:63
  • Methods

    @@ -309,7 +309,7 @@

    Returns void

    +
  • Defined in Raspberry.ts:114
  • +
  • Defined in Raspberry.ts:153
  • +
  • Defined in Raspberry.ts:143
  • +
  • Defined in Raspberry.ts:132
  • +
  • Defined in Raspberry.ts:160
  • +
  • Defined in Raspberry.ts:188
  • +
  • Defined in Raspberry.ts:172
  • +
  • Defined in Raspberry.ts:180
  • +
  • Defined in Raspberry.ts:123
  • +
  • Defined in Raspberry.ts:198
  • +
  • Defined in Raspberry.ts:106
  • Returns boolean

    +
  • Defined in Collidable.ts:9