From c6389fd9ba007681700bc7698fc2a2d1ab1b0ca4 Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 15:42:20 +0200 Subject: [PATCH 01/11] Implement parts of the voting ui --- backend/src/answer/answer.controller.ts | 23 ++ backend/src/answer/answer.service.ts | 9 + backend/src/types/voting.d.ts | 2 - backend/src/vote/vote.gateway.ts | 10 - frontend/index.html | 2 +- frontend/package-lock.json | 276 ------------------------ frontend/src/App.tsx | 73 ++++--- frontend/src/HomePage.tsx | 29 +++ frontend/src/PollPage.tsx | 141 ++++++++---- frontend/src/api/index.ts | 4 +- frontend/src/api/swagger.json | 30 +++ frontend/src/hooks/useSetPageTitle.ts | 15 ++ frontend/src/types/voting.d.ts | 2 - 13 files changed, 243 insertions(+), 373 deletions(-) create mode 100644 frontend/src/HomePage.tsx create mode 100644 frontend/src/hooks/useSetPageTitle.ts diff --git a/backend/src/answer/answer.controller.ts b/backend/src/answer/answer.controller.ts index 5a7b024..bb7f0ed 100644 --- a/backend/src/answer/answer.controller.ts +++ b/backend/src/answer/answer.controller.ts @@ -11,6 +11,7 @@ import { } from '@nestjs/common'; import { ApiCreatedResponse, + ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiTags, @@ -110,6 +111,28 @@ export class AnswerController { return this.sanitized(answer); } + @Post(':answerId/vote') + @ApiNoContentResponse({ + description: 'Vote recorded successfully', + }) + @ApiNotFoundResponse({ + type: null, + description: 'No answer with the given id has been found', + }) + async vote( + @Param('pollId') pollId: number, + @Param('questionId') questionId: number, + @Param('answerId') answerId: number, + @Res({ passthrough: true }) res: Response, + ): Promise { + const result = await this.answerService.increaseCount(answerId); + if (!result) { + res.status(404); + return; + } + res.status(204); + } + // @Patch(':answerId') // @ApiOkResponse({ // type: [Answer], diff --git a/backend/src/answer/answer.service.ts b/backend/src/answer/answer.service.ts index f0d3fa7..71b0a81 100644 --- a/backend/src/answer/answer.service.ts +++ b/backend/src/answer/answer.service.ts @@ -50,6 +50,15 @@ export class AnswerService { }); } + async increaseCount(answerId: number): Promise { + const result = await this.answerRepository.increment( + { id: answerId }, + 'count', + 1, + ); + return result.affected == 1; + } + // async update( // pollId: number, // questionId: number, diff --git a/backend/src/types/voting.d.ts b/backend/src/types/voting.d.ts index 890bb46..0302489 100644 --- a/backend/src/types/voting.d.ts +++ b/backend/src/types/voting.d.ts @@ -7,8 +7,6 @@ interface ServerToClientEvents { } interface VotePayload { - pollCode: string; - questionID: number; answerID: number; } diff --git a/backend/src/vote/vote.gateway.ts b/backend/src/vote/vote.gateway.ts index 03fed10..c1bdc20 100644 --- a/backend/src/vote/vote.gateway.ts +++ b/backend/src/vote/vote.gateway.ts @@ -13,16 +13,6 @@ interface ServerToClientEvents { update: (payload: UpdatePayload) => void; } -interface VotePayload { - pollCode: string; - questionID: number; - answerID: number; -} - -interface ClientToServerEvents { - vote: (payload: VotePayload) => void; -} - export type VoteServer = Server; @WebSocketGateway() diff --git a/frontend/index.html b/frontend/index.html index 324599d..1812070 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12,7 +12,7 @@ rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> - Vite App + SolidPolls
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a813c75..cda8578 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,6 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-query": "^3.39.0", - "socket.io": "^4.5.1", "socket.io-client": "^4.5.1", "wouter": "^2.7.5" }, @@ -1277,32 +1276,12 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "node_modules/@types/component-emitter": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", - "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "node_modules/@types/node": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", - "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -1586,18 +1565,6 @@ "node": ">=12.0.0" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", @@ -1766,14 +1733,6 @@ } ] }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -2016,11 +1975,6 @@ "integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==", "dev": true }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2177,26 +2131,6 @@ "safe-buffer": "~5.1.1" } }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -2349,26 +2283,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/engine.io-client": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz", @@ -4364,25 +4278,6 @@ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4440,14 +4335,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5259,27 +5146,6 @@ "node": ">=8" } }, - "node_modules/socket.io": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz", - "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" - }, "node_modules/socket.io-client": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz", @@ -5306,19 +5172,6 @@ "node": ">=10.0.0" } }, - "node_modules/socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "dependencies": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5669,14 +5522,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "2.9.9", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", @@ -6746,32 +6591,12 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "@types/component-emitter": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", - "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" - }, - "@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "@types/node": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", - "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" - }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -6955,15 +6780,6 @@ "resolve": "^1.22.0" } }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, "acorn": { "version": "8.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", @@ -7079,11 +6895,6 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, - "base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" - }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -7253,11 +7064,6 @@ "integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==", "dev": true }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7379,20 +7185,6 @@ "safe-buffer": "~5.1.1" } }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, "cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -7512,23 +7304,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", - "requires": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" - } - }, "engine.io-client": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz", @@ -8886,19 +8661,6 @@ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8944,11 +8706,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -9503,24 +9260,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "socket.io": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz", - "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==", - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.0.4" - } - }, - "socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" - }, "socket.io-client": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz", @@ -9543,16 +9282,6 @@ } } }, - "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -9814,11 +9543,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, "vite": { "version": "2.9.9", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 93a428a..855dc1f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,46 +1,45 @@ -import { pollsApi } from './api'; -import { useQuery } from 'react-query'; -import { Link, Route, Switch } from 'wouter'; +import { Link, Route, Switch, useLocation } from 'wouter'; import { useState } from 'react'; -import { Button, Container, Stack, TextField, Typography } from '@mui/material'; -import PollPage from "./PollPage"; +import { + AppBar, + Box, + Button, + Container, + Stack, + TextField, + Toolbar, + Typography, +} from '@mui/material'; +import PollPage from './PollPage'; +import HomePage from './HomePage'; -function HomePage() { - const [code, setCode] = useState(''); +function App() { return ( <> - Home - Please type in the poll code: - - setCode(event.target.value)} - /> - - - - + + + + + + SolidPolls + + + + + + + + + + + + ); } -function App() { - return ( - - {/*
- SolidPolls -
*/} - - - - - -
- ); -} - export default App; diff --git a/frontend/src/HomePage.tsx b/frontend/src/HomePage.tsx new file mode 100644 index 0000000..d8344ae --- /dev/null +++ b/frontend/src/HomePage.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import { Button, Stack, TextField, Typography } from '@mui/material'; +import { Link } from 'wouter'; +import useSetPageTitle from './hooks/useSetPageTitle'; + +export default function HomePage() { + const [code, setCode] = useState(''); + useSetPageTitle('Home'); + return ( + + + SolidPolls + + Please enter your poll code: + setCode(event.target.value)} + /> + + + + + ); +} diff --git a/frontend/src/PollPage.tsx b/frontend/src/PollPage.tsx index c292b5f..a730ff2 100644 --- a/frontend/src/PollPage.tsx +++ b/frontend/src/PollPage.tsx @@ -1,8 +1,17 @@ -import { useQuery } from 'react-query'; -import { pollsApi } from './api'; -import { Button, Stack, Typography } from '@mui/material'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { answersApi, pollsApi } from './api'; +import { + Box, + Button, + Card, + FormControlLabel, + Radio, + RadioGroup, + Stack, + Typography, +} from '@mui/material'; import { useEffect, useState } from 'react'; -import { Poll } from './client'; +import { Answer, Poll, Question } from './client'; import useVoteClient, { VoteClient } from './hooks/useVoteClient'; type PollPageProps = { @@ -10,39 +19,102 @@ type PollPageProps = { }; interface QuestionResultsProps { - poll: Poll; - questionIndex: number; - numVotes: number; + question: Question; } function QuestionResults(props: QuestionResultsProps) { return ( - The poll: {JSON.stringify(props.poll)} + The question: {JSON.stringify(props.question)} - Votes: {props.numVotes} ); } interface QuestionVoterProps { - poll: Poll; - questionIndex: number; - voteClient: VoteClient; + pollId: number; + question: Question; onAfterVote: () => void; } function QuestionVoter(props: QuestionVoterProps) { - const onVote = () => { - props.voteClient.emit('vote', { - pollCode: props.poll.code, - questionID: 0, - answerID: 0, - }); - props.onAfterVote(); - }; - return ; + const queryClient = useQueryClient(); + const { mutate } = useMutation( + 'vote', + (answerId: number) => + answersApi + .answerControllerVote({ + pollId: props.pollId, + questionId: props.question.id, + answerId, + }) + .then(() => queryClient.invalidateQueries('poll')), + { onSuccess: () => props.onAfterVote() }, + ); + const [selectedAnswerId, setSelectedAnswerId] = useState(-1); + return ( + + {props.question.text} + {props.question.answers.length === 0 ? ( + No answers were configured for this question. + ) : ( + + setSelectedAnswerId(+event.target.value)} + > + {props.question.answers.map((answer) => ( + } + label={answer.text} + value={answer.id} + /> + ))} + + + )} + + + ); +} + +interface LoadedPollProps { + poll: Poll; + voteClient: VoteClient; +} + +function LoadedPoll(props: LoadedPollProps) { + const [questionIndex] = useState(0); + const [voted, setVoted] = useState>(new Set()); + return ( + <> + + {props.poll.title} + + + {questionIndex < props.poll.questions.length ? ( + <> + {voted.has(questionIndex) ? ( + + ) : ( + { + setVoted(new Set([...voted, questionIndex])); + }} + /> + )} + + ) : ( + No questions have been added to this poll yet. + )} + + ); } export default function PollPage(props: PollPageProps) { @@ -51,15 +123,12 @@ export default function PollPage(props: PollPageProps) { () => pollsApi.pollControllerFindByCode({ code: props.params.code }), ); const voteClient = useVoteClient(props.params.code); - const [questionIndex] = useState(0); - const [voted, setVoted] = useState>(new Set()); - const [numVotes, setNumVotes] = useState(0); useEffect(() => { if (!voteClient) { return; } voteClient.on('update', (payload) => { - setNumVotes(payload.votes); + console.log('update', payload); }); return () => { voteClient.removeListener('update'); @@ -68,27 +137,11 @@ export default function PollPage(props: PollPageProps) { return ( <> - Poll Page {isLoading || (!voteClient && Loading ...)} - {isSuccess && - voteClient && - (voted.has(questionIndex) ? ( - - ) : ( - { - setVoted(new Set([...voted, questionIndex])); - }} - /> - ))} + {isSuccess && voteClient && ( + + )} {isError && ( Could not fetch resource: {JSON.stringify(error)} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index aefefb3..7b35dc4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,4 +1,4 @@ -import { Configuration, PollsApi } from '../client'; +import { AnswersApi, Configuration, PollsApi } from '../client'; function getBasePath() { if (import.meta.env.DEV) { @@ -12,3 +12,5 @@ const configuration = new Configuration({ }); export const pollsApi = new PollsApi(configuration); +export const questionsApi = new AnswersApi(configuration); +export const answersApi = new AnswersApi(configuration); diff --git a/frontend/src/api/swagger.json b/frontend/src/api/swagger.json index fe959bd..46cc62e 100644 --- a/frontend/src/api/swagger.json +++ b/frontend/src/api/swagger.json @@ -302,6 +302,36 @@ }, "tags": ["answers"] } + }, + "/polls/{pollId}/questions/{questionId}/answers/{answerId}/vote": { + "post": { + "operationId": "AnswerController_vote", + "parameters": [ + { + "name": "pollId", + "required": true, + "in": "path", + "schema": { "type": "number" } + }, + { + "name": "questionId", + "required": true, + "in": "path", + "schema": { "type": "number" } + }, + { + "name": "answerId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "204": { "description": "Vote recorded successfully" }, + "404": { "description": "No answer with the given id has been found" } + }, + "tags": ["answers"] + } } }, "info": { diff --git a/frontend/src/hooks/useSetPageTitle.ts b/frontend/src/hooks/useSetPageTitle.ts new file mode 100644 index 0000000..9c33054 --- /dev/null +++ b/frontend/src/hooks/useSetPageTitle.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +export default function useSetPageTitle(title: string) { + useEffect(() => { + const oldTitle = document.title; + setPageTitle(title); + return () => { + document.title = oldTitle; + }; + }, [title]); +} + +export function setPageTitle(title: string) { + document.title = title + ' | SolidPolls'; +} diff --git a/frontend/src/types/voting.d.ts b/frontend/src/types/voting.d.ts index 144b5d1..d2188f0 100644 --- a/frontend/src/types/voting.d.ts +++ b/frontend/src/types/voting.d.ts @@ -9,8 +9,6 @@ interface ServerToClientEvents { } interface VotePayload { - pollCode: string; - questionID: number; answerID: number; } From ace0e35c35a53cff05c41bd52c1ad82a1debcfd5 Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 15:55:31 +0200 Subject: [PATCH 02/11] Move components to separate files --- backend/src/answer/answer.module.ts | 3 +- backend/src/answer/answer.service.ts | 5 + backend/src/app.module.ts | 4 +- backend/src/vote/vote.gateway.ts | 15 +-- backend/src/vote/vote.module.ts | 8 ++ frontend/src/PollPage.tsx | 103 +------------------- frontend/src/components/LoadedPoll.tsx | 41 ++++++++ frontend/src/components/QuestionResults.tsx | 16 +++ frontend/src/components/QuestionVoter.tsx | 59 +++++++++++ 9 files changed, 141 insertions(+), 113 deletions(-) create mode 100644 backend/src/vote/vote.module.ts create mode 100644 frontend/src/components/LoadedPoll.tsx create mode 100644 frontend/src/components/QuestionResults.tsx create mode 100644 frontend/src/components/QuestionVoter.tsx diff --git a/backend/src/answer/answer.module.ts b/backend/src/answer/answer.module.ts index 0081062..8acd349 100644 --- a/backend/src/answer/answer.module.ts +++ b/backend/src/answer/answer.module.ts @@ -4,9 +4,10 @@ import { AnswerController } from './answer.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import Answer from '../models/answer'; import { QuestionModule } from '../question/question.module'; +import { VoteModule } from '../vote/vote.module'; @Module({ - imports: [TypeOrmModule.forFeature([Answer]), QuestionModule], + imports: [TypeOrmModule.forFeature([Answer]), QuestionModule, VoteModule], controllers: [AnswerController], providers: [AnswerService], exports: [AnswerService], diff --git a/backend/src/answer/answer.service.ts b/backend/src/answer/answer.service.ts index 71b0a81..d6fe7a7 100644 --- a/backend/src/answer/answer.service.ts +++ b/backend/src/answer/answer.service.ts @@ -5,6 +5,7 @@ import { QuestionService } from '../question/question.service'; import { Repository } from 'typeorm'; import { CreateAnswerDto } from './dto/create-answer.dto'; import { UpdateAnswerDto } from './dto/update-answer.dto'; +import { VoteGateway } from '../vote/vote.gateway'; @Injectable() export class AnswerService { @@ -13,6 +14,8 @@ export class AnswerService { private answerRepository: Repository, @Inject(QuestionService) private questionService: QuestionService, + @Inject(VoteGateway) + private voteGateway: VoteGateway, ) {} async create( @@ -56,6 +59,8 @@ export class AnswerService { 'count', 1, ); + this.voteGateway.notifyListeners(); + return result.affected == 1; } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 9a10990..501b638 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -5,6 +5,7 @@ import { PollModule } from './poll/poll.module'; import { VoteGateway } from './vote/vote.gateway'; import { QuestionModule } from './question/question.module'; import { AnswerModule } from './answer/answer.module'; +import { VoteModule } from './vote/vote.module'; @Module({ imports: [ @@ -12,8 +13,9 @@ import { AnswerModule } from './answer/answer.module'; PollModule, QuestionModule, AnswerModule, + VoteModule, ], controllers: [], - providers: [VoteGateway], + providers: [], }) export class AppModule {} diff --git a/backend/src/vote/vote.gateway.ts b/backend/src/vote/vote.gateway.ts index c1bdc20..d7cf860 100644 --- a/backend/src/vote/vote.gateway.ts +++ b/backend/src/vote/vote.gateway.ts @@ -5,24 +5,17 @@ import { } from '@nestjs/websockets'; import { Server } from 'socket.io'; -interface UpdatePayload { - votes: number; -} - interface ServerToClientEvents { - update: (payload: UpdatePayload) => void; + update: () => void; } -export type VoteServer = Server; +export type VoteServer = Server, ServerToClientEvents>; @WebSocketGateway() export class VoteGateway { @WebSocketServer() server: VoteServer; - private votes = 0; - @SubscribeMessage('vote') - handleMessage(client: any, payload: VotePayload): void { - this.votes++; - this.server.emit('update', { votes: this.votes }); + notifyListeners() { + this.server.emit('update'); } } diff --git a/backend/src/vote/vote.module.ts b/backend/src/vote/vote.module.ts new file mode 100644 index 0000000..bfc08c5 --- /dev/null +++ b/backend/src/vote/vote.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { VoteGateway } from './vote.gateway'; + +@Module({ + providers: [VoteGateway], + exports: [VoteGateway], +}) +export class VoteModule {} diff --git a/frontend/src/PollPage.tsx b/frontend/src/PollPage.tsx index a730ff2..a94932f 100644 --- a/frontend/src/PollPage.tsx +++ b/frontend/src/PollPage.tsx @@ -13,122 +13,25 @@ import { import { useEffect, useState } from 'react'; import { Answer, Poll, Question } from './client'; import useVoteClient, { VoteClient } from './hooks/useVoteClient'; +import LoadedPoll from './components/LoadedPoll'; type PollPageProps = { params: { code: string }; }; -interface QuestionResultsProps { - question: Question; -} - -function QuestionResults(props: QuestionResultsProps) { - return ( - - - The question: {JSON.stringify(props.question)} - - - ); -} - -interface QuestionVoterProps { - pollId: number; - question: Question; - onAfterVote: () => void; -} - -function QuestionVoter(props: QuestionVoterProps) { - const queryClient = useQueryClient(); - const { mutate } = useMutation( - 'vote', - (answerId: number) => - answersApi - .answerControllerVote({ - pollId: props.pollId, - questionId: props.question.id, - answerId, - }) - .then(() => queryClient.invalidateQueries('poll')), - { onSuccess: () => props.onAfterVote() }, - ); - const [selectedAnswerId, setSelectedAnswerId] = useState(-1); - return ( - - {props.question.text} - {props.question.answers.length === 0 ? ( - No answers were configured for this question. - ) : ( - - setSelectedAnswerId(+event.target.value)} - > - {props.question.answers.map((answer) => ( - } - label={answer.text} - value={answer.id} - /> - ))} - - - )} - - - ); -} - -interface LoadedPollProps { - poll: Poll; - voteClient: VoteClient; -} - -function LoadedPoll(props: LoadedPollProps) { - const [questionIndex] = useState(0); - const [voted, setVoted] = useState>(new Set()); - return ( - <> - - {props.poll.title} - - - {questionIndex < props.poll.questions.length ? ( - <> - {voted.has(questionIndex) ? ( - - ) : ( - { - setVoted(new Set([...voted, questionIndex])); - }} - /> - )} - - ) : ( - No questions have been added to this poll yet. - )} - - ); -} - export default function PollPage(props: PollPageProps) { const { isLoading, isSuccess, isError, data, error } = useQuery( ['poll', props.params.code], () => pollsApi.pollControllerFindByCode({ code: props.params.code }), ); + const queryClient = useQueryClient(); const voteClient = useVoteClient(props.params.code); useEffect(() => { if (!voteClient) { return; } voteClient.on('update', (payload) => { - console.log('update', payload); + queryClient.invalidateQueries(['poll', props.params.code]); }); return () => { voteClient.removeListener('update'); diff --git a/frontend/src/components/LoadedPoll.tsx b/frontend/src/components/LoadedPoll.tsx new file mode 100644 index 0000000..027008f --- /dev/null +++ b/frontend/src/components/LoadedPoll.tsx @@ -0,0 +1,41 @@ +import { Poll } from '../client'; +import { VoteClient } from '../hooks/useVoteClient'; +import { useState } from 'react'; +import { Typography } from '@mui/material'; +import QuestionVoter from './QuestionVoter'; +import QuestionResults from './QuestionResults'; + +interface LoadedPollProps { + poll: Poll; + voteClient: VoteClient; +} + +export default function LoadedPoll(props: LoadedPollProps) { + const [questionIndex] = useState(0); + const [voted, setVoted] = useState>(new Set()); + return ( + <> + + {props.poll.title} + + + {questionIndex < props.poll.questions.length ? ( + <> + {voted.has(questionIndex) ? ( + + ) : ( + { + setVoted(new Set([...voted, questionIndex])); + }} + /> + )} + + ) : ( + No questions have been added to this poll yet. + )} + + ); +} diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx new file mode 100644 index 0000000..808b3b1 --- /dev/null +++ b/frontend/src/components/QuestionResults.tsx @@ -0,0 +1,16 @@ +import { Question } from '../client'; +import { Stack, Typography } from '@mui/material'; + +interface QuestionResultsProps { + question: Question; +} + +export default function QuestionResults(props: QuestionResultsProps) { + return ( + + + The question: {JSON.stringify(props.question)} + + + ); +} diff --git a/frontend/src/components/QuestionVoter.tsx b/frontend/src/components/QuestionVoter.tsx new file mode 100644 index 0000000..056b4ef --- /dev/null +++ b/frontend/src/components/QuestionVoter.tsx @@ -0,0 +1,59 @@ +import { Question } from '../client'; +import { useMutation } from 'react-query'; +import { answersApi } from '../api'; +import { useState } from 'react'; +import { + Box, + Button, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; + +interface QuestionVoterProps { + pollId: number; + question: Question; + onAfterVote: () => void; +} + +export default function QuestionVoter(props: QuestionVoterProps) { + const { mutate } = useMutation( + 'vote', + (answerId: number) => + answersApi.answerControllerVote({ + pollId: props.pollId, + questionId: props.question.id, + answerId, + }), + { onSuccess: () => props.onAfterVote() }, + ); + const [selectedAnswerId, setSelectedAnswerId] = useState(-1); + return ( + + {props.question.text} + {props.question.answers.length === 0 ? ( + No answers were configured for this question. + ) : ( + + setSelectedAnswerId(+event.target.value)} + > + {props.question.answers.map((answer) => ( + } + label={answer.text} + value={answer.id} + /> + ))} + + + )} + + + ); +} From 6112f6e5f4c24c7c6dbd16d4c18009966db25278 Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 15:55:54 +0200 Subject: [PATCH 03/11] Remove unused import --- backend/src/vote/vote.gateway.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/vote/vote.gateway.ts b/backend/src/vote/vote.gateway.ts index d7cf860..b029839 100644 --- a/backend/src/vote/vote.gateway.ts +++ b/backend/src/vote/vote.gateway.ts @@ -1,8 +1,4 @@ -import { - SubscribeMessage, - WebSocketGateway, - WebSocketServer, -} from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server } from 'socket.io'; interface ServerToClientEvents { From 687936ef62d971e431ffb121873652a1b8bbf1b4 Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 15:58:05 +0200 Subject: [PATCH 04/11] Remove some unused imports --- backend/src/app.module.ts | 1 - frontend/src/PollPage.tsx | 22 ++++++---------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 501b638..9c35c04 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import options from './config/ormconfig'; import { PollModule } from './poll/poll.module'; -import { VoteGateway } from './vote/vote.gateway'; import { QuestionModule } from './question/question.module'; import { AnswerModule } from './answer/answer.module'; import { VoteModule } from './vote/vote.module'; diff --git a/frontend/src/PollPage.tsx b/frontend/src/PollPage.tsx index a94932f..2d79780 100644 --- a/frontend/src/PollPage.tsx +++ b/frontend/src/PollPage.tsx @@ -1,18 +1,8 @@ -import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { answersApi, pollsApi } from './api'; -import { - Box, - Button, - Card, - FormControlLabel, - Radio, - RadioGroup, - Stack, - Typography, -} from '@mui/material'; -import { useEffect, useState } from 'react'; -import { Answer, Poll, Question } from './client'; -import useVoteClient, { VoteClient } from './hooks/useVoteClient'; +import { useQuery, useQueryClient } from 'react-query'; +import { pollsApi } from './api'; +import { Typography } from '@mui/material'; +import { useEffect } from 'react'; +import useVoteClient from './hooks/useVoteClient'; import LoadedPoll from './components/LoadedPoll'; type PollPageProps = { @@ -30,7 +20,7 @@ export default function PollPage(props: PollPageProps) { if (!voteClient) { return; } - voteClient.on('update', (payload) => { + voteClient.on('update', () => { queryClient.invalidateQueries(['poll', props.params.code]); }); return () => { From 45e423f63b03bf335e8bdab2d4da2b7aa4eb795f Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 16:15:53 +0200 Subject: [PATCH 05/11] Handle non-existing poll more gracefully --- frontend/src/HomePage.tsx | 67 ++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/frontend/src/HomePage.tsx b/frontend/src/HomePage.tsx index d8344ae..da11f4c 100644 --- a/frontend/src/HomePage.tsx +++ b/frontend/src/HomePage.tsx @@ -1,29 +1,60 @@ import { useState } from 'react'; -import { Button, Stack, TextField, Typography } from '@mui/material'; -import { Link } from 'wouter'; +import { + Alert, + Button, + Snackbar, + Stack, + TextField, + Typography, +} from '@mui/material'; import useSetPageTitle from './hooks/useSetPageTitle'; +import { pollsApi } from './api'; +import { useLocation } from 'wouter'; export default function HomePage() { const [code, setCode] = useState(''); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [, setLocation] = useLocation(); useSetPageTitle('Home'); return ( - - - SolidPolls - - Please enter your poll code: - setCode(event.target.value)} - /> - - - - + + ); } From 7bb3597ebbba62371637796a81f5f937e3aee60c Mon Sep 17 00:00:00 2001 From: Richard Wohlbold Date: Sun, 26 Jun 2022 16:18:52 +0200 Subject: [PATCH 06/11] Fix tests --- backend/src/answer/answer.controller.spec.ts | 2 ++ backend/src/answer/answer.service.spec.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/backend/src/answer/answer.controller.spec.ts b/backend/src/answer/answer.controller.spec.ts index 30a4f1f..6c3f127 100644 --- a/backend/src/answer/answer.controller.spec.ts +++ b/backend/src/answer/answer.controller.spec.ts @@ -4,6 +4,7 @@ import { Repository } from 'typeorm'; import { AnswerController } from './answer.controller'; import { AnswerService } from './answer.service'; import { PollService } from '../poll/poll.service'; +import { VoteGateway } from '../vote/vote.gateway'; describe('AnswerController', () => { let controller: AnswerController; @@ -15,6 +16,7 @@ describe('AnswerController', () => { AnswerService, QuestionService, PollService, + VoteGateway, { provide: 'AnswerRepository', useClass: Repository, diff --git a/backend/src/answer/answer.service.spec.ts b/backend/src/answer/answer.service.spec.ts index 819185a..2485476 100644 --- a/backend/src/answer/answer.service.spec.ts +++ b/backend/src/answer/answer.service.spec.ts @@ -3,6 +3,7 @@ import { QuestionService } from '../question/question.service'; import { Repository } from 'typeorm'; import { AnswerService } from './answer.service'; import { PollService } from '../poll/poll.service'; +import { VoteGateway } from '../vote/vote.gateway'; describe('AnswerService', () => { let service: AnswerService; @@ -12,6 +13,7 @@ describe('AnswerService', () => { providers: [ AnswerService, QuestionService, + VoteGateway, PollService, { provide: 'AnswerRepository', From b8f075bfae78a3be5ca087ed3d134cd61af1f728 Mon Sep 17 00:00:00 2001 From: Aaron Schlitt Date: Sun, 26 Jun 2022 18:24:42 +0200 Subject: [PATCH 07/11] Display a graph of voting results --- frontend/package-lock.json | 195 ++++++++++++++++++++ frontend/package.json | 2 + frontend/src/components/QuestionResults.tsx | 54 +++++- 3 files changed, 246 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cda8578..97a01e8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,9 @@ "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.8.2", "@mui/material": "^5.8.2", + "apexcharts": "^3.35.3", "react": "^18.0.0", + "react-apexcharts": "^1.4.0", "react-dom": "^18.0.0", "react-query": "^3.39.0", "socket.io-client": "^4.5.1", @@ -1637,6 +1639,19 @@ "node": ">=4" } }, + "node_modules/apexcharts": { + "version": "3.35.3", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.35.3.tgz", + "integrity": "sha512-UDlxslJr3DG63I/SgoiivIu4lpP25GMaKFK8NvCHmTksTQshx4ng3oPPrYvdsBFOvD/ajPYIh/p7rNB0jq8vXg==", + "dependencies": { + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4812,6 +4827,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-apexcharts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz", + "integrity": "sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==", + "dependencies": { + "prop-types": "^15.5.7" + }, + "peerDependencies": { + "apexcharts": "^3.18.0", + "react": ">=0.13" + } + }, "node_modules/react-dom": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", @@ -5336,6 +5363,89 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6828,6 +6938,19 @@ "color-convert": "^1.9.0" } }, + "apexcharts": { + "version": "3.35.3", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.35.3.tgz", + "integrity": "sha512-UDlxslJr3DG63I/SgoiivIu4lpP25GMaKFK8NvCHmTksTQshx4ng3oPPrYvdsBFOvD/ajPYIh/p7rNB0jq8vXg==", + "requires": { + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -9027,6 +9150,14 @@ "loose-envify": "^1.1.0" } }, + "react-apexcharts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz", + "integrity": "sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==", + "requires": { + "prop-types": "^15.5.7" + } + }, "react-dom": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", @@ -9398,6 +9529,70 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "requires": { + "svg.js": "^2.0.1" + } + }, + "svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "requires": { + "svg.js": ">=2.3.x" + } + }, + "svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "requires": { + "svg.js": "^2.2.5" + } + }, + "svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "requires": { + "svg.js": "^2.4.0" + } + }, + "svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "requires": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "dependencies": { + "svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "requires": { + "svg.js": "^2.2.5" + } + } + } + }, + "svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "requires": { + "svg.js": "^2.6.5" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d813396..f32b446 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,9 @@ "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.8.2", "@mui/material": "^5.8.2", + "apexcharts": "^3.35.3", "react": "^18.0.0", + "react-apexcharts": "^1.4.0", "react-dom": "^18.0.0", "react-query": "^3.39.0", "socket.io-client": "^4.5.1", diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx index 808b3b1..2d6a611 100644 --- a/frontend/src/components/QuestionResults.tsx +++ b/frontend/src/components/QuestionResults.tsx @@ -1,16 +1,60 @@ -import { Question } from '../client'; -import { Stack, Typography } from '@mui/material'; +import { Answer, Question } from '../client'; +import { Container, Stack } from '@mui/material'; +import Chart from 'react-apexcharts'; interface QuestionResultsProps { question: Question; } export default function QuestionResults(props: QuestionResultsProps) { + const series = []; + const labels = []; + + // The order of these changes on database changes by voting, to maintain a stable chart, we need to sort the answers by name + const answers = props.question.answers.sort(answerSorter); + + for (const answer of answers) { + series.push(answer.count); + labels.push(answer.text); + } + + const options = { + labels, + theme: { + mode: 'dark' as const, + }, + chart: { + background: 'none', + fontFamily: 'Roboto', + }, + legend: { + position: 'top' as const, + }, + plotOptions: { + pie: { + donut: { + labels: { + show: true, + total: { + show: true, + }, + }, + }, + }, + }, + }; + return ( - - The question: {JSON.stringify(props.question)} - + + + ); } + +function answerSorter(a1: Answer, a2: Answer) { + if (a1.text > a2.text) return 1; + if (a1.text < a2.text) return -1; + return 0; +} From 305225a96de48a635fcdba33c89cca2e97b0a789 Mon Sep 17 00:00:00 2001 From: Aaron Schlitt Date: Sun, 26 Jun 2022 18:30:15 +0200 Subject: [PATCH 08/11] Add the label for the question in results view. --- frontend/src/components/QuestionResults.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx index 2d6a611..51c56d1 100644 --- a/frontend/src/components/QuestionResults.tsx +++ b/frontend/src/components/QuestionResults.tsx @@ -1,5 +1,5 @@ import { Answer, Question } from '../client'; -import { Container, Stack } from '@mui/material'; +import { Container, Stack, Typography } from '@mui/material'; import Chart from 'react-apexcharts'; interface QuestionResultsProps { @@ -9,6 +9,7 @@ interface QuestionResultsProps { export default function QuestionResults(props: QuestionResultsProps) { const series = []; const labels = []; + const title = props.question.text; // The order of these changes on database changes by voting, to maintain a stable chart, we need to sort the answers by name const answers = props.question.answers.sort(answerSorter); @@ -46,6 +47,7 @@ export default function QuestionResults(props: QuestionResultsProps) { return ( + {props.question.text} From b9af4e09050036913419a251a3301358256c7a4e Mon Sep 17 00:00:00 2001 From: Aaron Schlitt Date: Mon, 27 Jun 2022 09:27:07 +0200 Subject: [PATCH 09/11] Code style --- frontend/src/components/QuestionResults.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx index 51c56d1..08684ca 100644 --- a/frontend/src/components/QuestionResults.tsx +++ b/frontend/src/components/QuestionResults.tsx @@ -7,17 +7,13 @@ interface QuestionResultsProps { } export default function QuestionResults(props: QuestionResultsProps) { - const series = []; - const labels = []; const title = props.question.text; // The order of these changes on database changes by voting, to maintain a stable chart, we need to sort the answers by name const answers = props.question.answers.sort(answerSorter); - for (const answer of answers) { - series.push(answer.count); - labels.push(answer.text); - } + const series = answers.map((answer) => answer.count); + const labels = answers.map((answer) => answer.text); const options = { labels, From ba0fbbc57765b74685ab50e2da0be35d9742c65a Mon Sep 17 00:00:00 2001 From: Aaron Schlitt Date: Mon, 27 Jun 2022 09:33:33 +0200 Subject: [PATCH 10/11] Remove unused variable --- frontend/src/components/QuestionResults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx index 08684ca..a45cc5c 100644 --- a/frontend/src/components/QuestionResults.tsx +++ b/frontend/src/components/QuestionResults.tsx @@ -43,7 +43,7 @@ export default function QuestionResults(props: QuestionResultsProps) { return ( - {props.question.text} + {title} From 2bab69aa1a5c9d3c14ed14f3bf4e1b51dcc9d3c2 Mon Sep 17 00:00:00 2001 From: Aaron Schlitt Date: Mon, 27 Jun 2022 21:47:45 +0200 Subject: [PATCH 11/11] Finish Merge --- frontend/src/components/QuestionResults.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/QuestionResults.tsx b/frontend/src/components/QuestionResults.tsx index c1f4e4a..a45cc5c 100644 --- a/frontend/src/components/QuestionResults.tsx +++ b/frontend/src/components/QuestionResults.tsx @@ -1,11 +1,6 @@ -<<<<<<< HEAD import { Answer, Question } from '../client'; import { Container, Stack, Typography } from '@mui/material'; import Chart from 'react-apexcharts'; -======= -import { Question } from '../client'; -import { Stack, Typography } from '@mui/material'; ->>>>>>> main interface QuestionResultsProps { question: Question;