-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
- Loading branch information
1 parent
7f4a747
commit b84ee98
Showing
11 changed files
with
292 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 123 additions & 29 deletions
152
apps/frontend/src/app/features/voting/results/results.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,127 @@ | ||
<div class="flex justify-center mt-20"> | ||
<fieldset class="w-11/12"> | ||
<div class="overflow-hidden bg-white sm:rounded-lg sm:shadow"> | ||
<div class="border-b border-gray-200 bg-white px-4 py-5 sm:px-6"> | ||
<h3 class="font-semibold leading-6 text-gray-900 text-xl"> | ||
{{ dataArr[0] }} - Results | ||
</h3> | ||
@if (hasError) { | ||
<app-error | ||
[title]="'Error occurred'" | ||
[message]="errorMessage" | ||
[action]="'Go to Home'" | ||
[route]="'/'" | ||
aria-live="assertive" | ||
aria-label="Error occurred" | ||
></app-error> | ||
} @if (isLoading) { | ||
<app-loading aria-live="polite" aria-label="Loading results..."></app-loading> | ||
} @if (!isLoading && !hasError) { | ||
<div class="mx-auto max-w-7xl" role="region" aria-labelledby="results-title"> | ||
<div class="mx-1 sm:mx-1.5 md:mx-3 lg:mx-5 xl:mx-0"> | ||
<h1 | ||
id="results-title" | ||
class="text-2xl font-semibold leading-7 text-gray-900" | ||
> | ||
Voting Results | ||
</h1> | ||
|
||
<div class="py-3"> | ||
<h2 | ||
id="vote-question" | ||
class="text-xl font-semibold leading-7 text-gray-900" | ||
> | ||
{{ dataArr[0] }} | ||
</h2> | ||
<p id="vote-description" class="mt-1 text-sm leading-6 text-gray-600"> | ||
{{ dataArr[1] }} | ||
</p> | ||
</div> | ||
|
||
<div class="relative mt-6" role="separator" aria-orientation="horizontal"> | ||
<div class="absolute inset-0 flex items-center" aria-hidden="true"> | ||
<div class="w-full border-t border-gray-300"></div> | ||
</div> | ||
<ul role="list" class="divide-y divide-gray-200"> | ||
@for (result of resultArr; track result.key){ | ||
<li> | ||
<a href="#" class="block hover:bg-gray-50"> | ||
<div class="px-4 py-4 sm:px-6"> | ||
<div class="flex cursor-pointer justify-between items-center"> | ||
<div> | ||
<p class="text-gray-950 text-xl">{{ result.key }}</p> | ||
</div> | ||
|
||
<div class="ml-2 flex flex-shrink-0"> | ||
<span | ||
class="w-12 h-12 inline-flex justify-center items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20" | ||
>{{ result.val }}</span | ||
> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
@if (!hasAlreadyVoted) { | ||
<div class="pt-6"> | ||
<div | ||
aria-live="assertive" | ||
class="bg-gray-100 border border-gray-400 text-gray-900 px-4 py-3 rounded" | ||
role="alert" | ||
> | ||
<h2 class="font-bold">Your opinion?</h2> | ||
<p class="text-sm"> | ||
It looks like you didn't cast your vote yet. Give your opinion on this | ||
vote - every opinion matters! | ||
<a | ||
[href]="'/voting/' + voteId" | ||
class="hover:underline font-medium cursor-pointer text-gray-900 hover:text-gray-600 focus:outline-none focus:underline focus:text-gray-600" | ||
role="link" | ||
tabindex="0" | ||
aria-label="Cast your vote" | ||
> | ||
Cast your vote | ||
</a> | ||
</li> | ||
} | ||
</ul> | ||
</p> | ||
</div> | ||
</div> | ||
} | ||
|
||
<div class="relative mt-6" role="separator" aria-orientation="horizontal"> | ||
<div class="absolute inset-0 flex items-center" aria-hidden="true"> | ||
<div class="w-full border-t border-gray-300"></div> | ||
</div> | ||
</div> | ||
|
||
<div class="pt-6 space-y-4" role="list" aria-labelledby="results-title"> | ||
@for (result of resultArr; track $index) { | ||
<div class="relative bg-gray-100 rounded-lg p-4" role="listitem"> | ||
<div class="flex justify-between"> | ||
<p id="option-{{ $index }}" class="text-gray-700 text-lg break-words"> | ||
{{ result.key }} | ||
</p> | ||
<p id="votes-{{ $index }}" class="text-gray-500"> | ||
{{ result.val }} votes | ||
</p> | ||
</div> | ||
<div class="mt-2 h-4 bg-gray-300 rounded-lg"> | ||
<div | ||
class="h-4 rounded-lg bg-indigo-600" | ||
role="progressbar" | ||
[style.width]="(parseInt(result.val, 10) / totalVotes) * 100 + '%'" | ||
></div> | ||
</div> | ||
</div> | ||
} | ||
</div> | ||
|
||
<div class="mt-3 text-center text-sm text-gray-500"> | ||
<p>Total votes: {{ totalVotes }}</p> | ||
</div> | ||
|
||
<div class="relative mt-6" role="separator" aria-orientation="horizontal"> | ||
<div class="absolute inset-0 flex items-center" aria-hidden="true"> | ||
<div class="w-full border-t border-gray-300"></div> | ||
</div> | ||
</div> | ||
|
||
<div class="pt-6"> | ||
<div | ||
aria-live="assertive" | ||
class="bg-gray-100 border border-gray-400 text-gray-900 px-4 py-3 rounded" | ||
role="alert" | ||
> | ||
<h2 class="font-bold">Export Voting Data</h2> | ||
<p class="text-sm"> | ||
You can export the voting results to a CSV file for further analysis | ||
or record-keeping. | ||
<button | ||
(click)="exportToCSV()" | ||
class="hover:underline font-medium cursor-pointer text-gray-900 hover:text-gray-600 focus:outline-none focus:underline focus:text-gray-600" | ||
role="link" | ||
tabindex="0" | ||
aria-label="Export results" | ||
> | ||
Export Results | ||
</button> | ||
</p> | ||
</div> | ||
</div> | ||
</fieldset> | ||
</div> | ||
</div> | ||
} |
153 changes: 146 additions & 7 deletions
153
apps/frontend/src/app/features/voting/results/results.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> = [] | ||
@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<string> = [] | ||
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<void> { | ||
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<void> { | ||
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<void> { | ||
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<void> { | ||
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<void> { | ||
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +0,0 @@ | ||
.fixed-center { | ||
position: fixed; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
} | ||
Oops, something went wrong.