From b84ee987d33eed32a077b5371605b81be1dc1e6d Mon Sep 17 00:00:00 2001 From: Daniel Jancar <112062588+danieljancar@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:39:21 +0200 Subject: [PATCH] feat: create results page (#47) * feat: create results page * fix: change option description to new max lenght * feat: add cast vote banner to results if user hasn't voted yet --- apps/frontend/src/app/app.routes.ts | 2 +- .../features/voting/cast/cast.component.ts | 2 +- .../voting/create/create.component.html | 10 +- .../voting/create/create.component.ts | 2 +- .../voting/results/results.component.html | 152 +++++++++++++---- .../voting/results/results.component.ts | 153 +++++++++++++++++- .../voting/thanks/thanks.component.css | 6 - .../voting/thanks/thanks.component.html | 48 +----- .../voting/thanks/thanks.component.ts | 9 +- .../app/features/voting/voting.component.html | 2 +- .../app/features/voting/voting.component.ts | 18 --- 11 files changed, 292 insertions(+), 112 deletions(-) diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts index cdb52f6..9923396 100644 --- a/apps/frontend/src/app/app.routes.ts +++ b/apps/frontend/src/app/app.routes.ts @@ -44,7 +44,7 @@ export const appRoutes: Route[] = [ canDeactivate: [UnsavedChangesGuard], }, { - path: 'results/:id', + path: 'r/:id', component: ResultsComponent, }, { diff --git a/apps/frontend/src/app/features/voting/cast/cast.component.ts b/apps/frontend/src/app/features/voting/cast/cast.component.ts index 4a0aacc..f26f0a7 100644 --- a/apps/frontend/src/app/features/voting/cast/cast.component.ts +++ b/apps/frontend/src/app/features/voting/cast/cast.component.ts @@ -107,7 +107,7 @@ export class CastComponent implements OnChanges { return } } - this.router.navigate(['/voting/results', this.voteId]) + this.router.navigate(['/voting/r', this.voteId]) } public errorAction(): void { diff --git a/apps/frontend/src/app/features/voting/create/create.component.html b/apps/frontend/src/app/features/voting/create/create.component.html index 760feb7..a1739c5 100644 --- a/apps/frontend/src/app/features/voting/create/create.component.html +++ b/apps/frontend/src/app/features/voting/create/create.component.html @@ -150,18 +150,18 @@ @for (option of options.controls; track $index) {
- @if (voteForm.value.options[$index].option.length > 45) { + @if (voteForm.value.options[$index].option.length > 30) { - {{ voteForm.value.options[$index].option.length }}/50 + {{ voteForm.value.options[$index].option.length }}/32 }
@@ -208,7 +208,7 @@ } @if (voteForm.hasError('maxOptionLength', ['options'])) {

- Each option must be less than 50 characters. + Each option must be less than 32 characters.

} @if (options.controls.length < 5) { diff --git a/apps/frontend/src/app/features/voting/create/create.component.ts b/apps/frontend/src/app/features/voting/create/create.component.ts index 81f7fff..742e180 100644 --- a/apps/frontend/src/app/features/voting/create/create.component.ts +++ b/apps/frontend/src/app/features/voting/create/create.component.ts @@ -80,7 +80,7 @@ export class CreateComponent Validators.minLength(2), Validators.maxLength(5), noDuplicateInOptions(), - maxOptionLengthValidator(50), + maxOptionLengthValidator(32), ], ), }, diff --git a/apps/frontend/src/app/features/voting/results/results.component.html b/apps/frontend/src/app/features/voting/results/results.component.html index b550640..426d1a0 100644 --- a/apps/frontend/src/app/features/voting/results/results.component.html +++ b/apps/frontend/src/app/features/voting/results/results.component.html @@ -1,33 +1,127 @@ -
-
-
-
-

- {{ dataArr[0] }} - Results -

+@if (hasError) { + +} @if (isLoading) { + +} @if (!isLoading && !hasError) { +
+
+

+ Voting Results +

+ +
+

+ {{ dataArr[0] }} +

+

+ {{ dataArr[1] }} +

+
+ + + + @if (!hasAlreadyVoted) { + + } + + + +
+ @for (result of resultArr; track $index) { +
+
+

+ {{ result.key }} +

+

+ {{ result.val }} votes +

+
+
+
+
+
+ } +
+ +
+

Total votes: {{ totalVotes }}

+
+ + + +
+
-
+
+} diff --git a/apps/frontend/src/app/features/voting/results/results.component.ts b/apps/frontend/src/app/features/voting/results/results.component.ts index 0f7e33c..e0a4fc9 100644 --- a/apps/frontend/src/app/features/voting/results/results.component.ts +++ b/apps/frontend/src/app/features/voting/results/results.component.ts @@ -1,14 +1,153 @@ -import { Component, Input } from '@angular/core' +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { SorobanRpc } from '@stellar/stellar-sdk' +import { Keypair } from '@stellar/typescript-wallet-sdk' +import { GetVoteResultsService } from '../../../core/stellar/getVoteResults.service' +import { VoteConfigService } from '../../../core/vote-transaction.service' +import { ErrorComponent } from '../../../shared/feedback/error/error.component' +import { LoadingComponent } from '../../../shared/feedback/loading/loading.component' +import { GetVoteService } from '../../../core/stellar/getVote.service' +import { Subscription } from 'rxjs' +import { CheckUserVotedService } from '../../../core/stellar/checkUserVoted.service' @Component({ selector: 'app-results', standalone: true, - imports: [], templateUrl: './results.component.html', - styleUrl: './results.component.css', + styleUrls: ['./results.component.css'], + imports: [ErrorComponent, LoadingComponent], }) -export class ResultsComponent { - @Input({ required: true }) voteId = '' - @Input({ required: true }) dataArr: Array = [] - @Input({ required: true }) resultArr: Array<{ key: string; val: string }> = [] +export class ResultsComponent implements OnInit, OnDestroy { + public hasAlreadyVoted = false + public isLoading = true + public hasError = false + public errorMessage = '' + public voteId = '' + public dataArr: Array = [] + public resultArr: Array<{ key: string; val: string }> = [] + public totalVotes = 0 + protected readonly parseInt = parseInt + private sourceKeypair!: Keypair + private server!: SorobanRpc.Server + private routeSubscription!: Subscription + + constructor( + private route: ActivatedRoute, + private router: Router, + private getVoteResultsService: GetVoteResultsService, + private getVoteService: GetVoteService, + private voteConfigService: VoteConfigService, + private checkUserVotedService: CheckUserVotedService, + ) {} + + public async ngOnInit(): Promise { + try { + const config = await this.voteConfigService.getBaseVoteConfig() + this.server = config.server + this.sourceKeypair = config.sourceKeypair + + this.routeSubscription = this.route.params.subscribe(async params => { + this.voteId = params['id'] + this.isLoading = true + this.hasError = false + await this.loadVoteData() + }) + } catch (error) { + this.handleError('Failed to initialize results component.', error) + } + } + + public exportToCSV(): void { + const headers = ['Option', 'Votes'] + const rows = this.resultArr.map(result => [result.key, result.val]) + + const csvContent = [ + headers.join(','), + ...rows.map(row => row.join(',')), + ].join('\n') + + const blob = new Blob([csvContent], { type: 'text/csv' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `vote_results_${this.voteId}.csv` + a.click() + window.URL.revokeObjectURL(url) + } + + ngOnDestroy(): void { + if (this.routeSubscription) { + this.routeSubscription.unsubscribe() + } + } + + private async loadVoteData(): Promise { + try { + await this.getVoteData() + await this.getVoteResults() + } catch (error) { + this.handleError('Failed to load vote data.', error) + } finally { + this.isLoading = false + } + } + + private async getVoteData(): Promise { + const result = await this.getVoteService.getVote( + this.server, + this.sourceKeypair, + this.voteId, + ) + if (result?.hasError) { + throw new Error(result.errorMessage) + } + if (result) { + this.dataArr = result.dataArr + } + } + + private async getVoteResults(): Promise { + const result = await this.getVoteResultsService.getVoteResults( + this.server, + this.sourceKeypair, + this.voteId, + ) + + if (result?.hasError) { + throw new Error(result.errorMessage) + } + + if (result) { + this.resultArr = result.dataArr + this.totalVotes = this.resultArr.reduce( + (sum, option) => sum + parseInt(option.val, 10), + 0, + ) + } + } + + private async checkIfUserHasVoted(): Promise { + const result = await this.checkUserVotedService.checkIfUserVoted( + this.server, + this.sourceKeypair, + this.voteId, + this.sourceKeypair.publicKey(), + ) + if (result?.hasError) { + this.hasError = true + this.errorMessage = result.errorMessage + this.isLoading = false + return + } + if (result) { + this.hasAlreadyVoted = result.hasVoted + } + } + + private handleError(message: string, error: any): void { + console.error(message, error) + this.hasError = true + this.errorMessage = message + this.isLoading = false + } } diff --git a/apps/frontend/src/app/features/voting/thanks/thanks.component.css b/apps/frontend/src/app/features/voting/thanks/thanks.component.css index 0161af9..e69de29 100644 --- a/apps/frontend/src/app/features/voting/thanks/thanks.component.css +++ b/apps/frontend/src/app/features/voting/thanks/thanks.component.css @@ -1,6 +0,0 @@ -.fixed-center { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} diff --git a/apps/frontend/src/app/features/voting/thanks/thanks.component.html b/apps/frontend/src/app/features/voting/thanks/thanks.component.html index 1073439..8788d59 100644 --- a/apps/frontend/src/app/features/voting/thanks/thanks.component.html +++ b/apps/frontend/src/app/features/voting/thanks/thanks.component.html @@ -1,40 +1,8 @@ -
-
- - - -

Thank You !

-

Thank you for voting on this poll!

- - - - - Home - -
-
+ diff --git a/apps/frontend/src/app/features/voting/thanks/thanks.component.ts b/apps/frontend/src/app/features/voting/thanks/thanks.component.ts index 4fb5343..3dec456 100644 --- a/apps/frontend/src/app/features/voting/thanks/thanks.component.ts +++ b/apps/frontend/src/app/features/voting/thanks/thanks.component.ts @@ -1,10 +1,13 @@ -import { Component } from '@angular/core' +import { Component, Input } from '@angular/core' +import { SuccessComponent } from '../../../shared/feedback/success/success.component' @Component({ selector: 'app-thanks', standalone: true, - imports: [], + imports: [SuccessComponent], templateUrl: './thanks.component.html', styleUrl: './thanks.component.css', }) -export class ThanksComponent {} +export class ThanksComponent { + @Input({ required: true }) voteId = '' +} diff --git a/apps/frontend/src/app/features/voting/voting.component.html b/apps/frontend/src/app/features/voting/voting.component.html index eed13ce..8cef8d1 100644 --- a/apps/frontend/src/app/features/voting/voting.component.html +++ b/apps/frontend/src/app/features/voting/voting.component.html @@ -1,5 +1,5 @@ @if (hasAlreadyVoted) { - + } @else { @if (isLoading) { } @if (!isLoading && !hasError) { diff --git a/apps/frontend/src/app/features/voting/voting.component.ts b/apps/frontend/src/app/features/voting/voting.component.ts index 28f798a..08becb4 100644 --- a/apps/frontend/src/app/features/voting/voting.component.ts +++ b/apps/frontend/src/app/features/voting/voting.component.ts @@ -88,7 +88,6 @@ export class VotingComponent implements OnInit, OnDestroy { await this.getVoteData() await this.checkIfUserHasVoted() await this.getVoteOptions() - await this.getVoteResults() this.isLoading = false } @@ -126,23 +125,6 @@ export class VotingComponent implements OnInit, OnDestroy { } } - private async getVoteResults(): Promise { - const result = await this.getVoteResultsService.getVoteResults( - this.server, - this.sourceKeypair, - this.voteId, - ) - if (result?.hasError) { - this.hasError = true - this.errorMessage = result.errorMessage - this.isLoading = false - return - } - if (result) { - this.resultArr = result.dataArr - } - } - private async checkIfUserHasVoted(): Promise { const result = await this.checkUserVotedService.checkIfUserVoted( this.server,