diff --git a/README.md b/README.md index dd1bff2..e09ec58 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ --- -This project is part of the [Stellar Smart Contract Challenge](https://dev.to/challenges/stellar) on [Dev.to](https://dev.to/). We built a blockchain-based voting platform using Stellar Smart Contracts. VoteVault is a secure and transparent voting platform that allows users to create and participate in voting processes. Find out more about our journey building VoteVault in the [Dev.to post]() or visit the [VoteVault website](). +This project is part of the [Stellar Smart Contract Challenge](https://dev.to/challenges/stellar) on [Dev.to](https://dev.to/). + +We built a blockchain-based voting platform using Stellar Smart Contracts. VoteVault is a secure and transparent voting platform that allows users to create and participate in voting processes. Find out more about our journey building VoteVault in the [Dev.to post]() or visit the [VoteVault website](). # Table of Contents @@ -29,6 +31,7 @@ This project is part of the [Stellar Smart Contract Challenge](https://dev.to/ch - [User Stories](#user-stories) - [Use Cases](#use-cases) - [Known Issues](#known-issues) + - [Future Features](#future-features) - [Contributing](#contributing) - [Developer Guidelines and Repository Setup](#developer-guidelines-and-repository-setup) - [Development Previews](#development-previews) @@ -77,7 +80,7 @@ VoteVault integrating Stellar Smart Contracts could be used in various scenarios - **Community Decisions**: Communities could use VoteVault to make decisions on community projects or initiatives. - **Controversial Opinions**: People could use VoteVault to express their opinions on controversial topics. -Based on this evaluation, the product sets a base source code for enhancing the platform with more features. Such as using VoteVault "core" as a base for a more complex voting system, like government elections with each citizen being registered once (which would kill the anonymity of the votes - _sigh_) and more. This provides a real-world use case for the Stellar Smart Contracts and the VoteVault platform. +Based on this evaluation, the product sets a base source code for enhancing the platform with more features. Such as using VoteVault "core" as a base for a more complex voting system, like government elections with each citizen being registered once (which would kill the anonymity of the votes - _sigh_) or other useful extensions of our application. This provides a real-world use case for the Stellar Smart Contracts and the VoteVault platform. ## Known Issues @@ -85,7 +88,14 @@ As this project was developed in a short amount of time, there are some known is - **Accessibility**: The platform is not fully accessible yet. We tried to implement some accessibility features, but there is still some work to do. - **Error Handling**: The error handling is not perfect yet, but already pretty good. -- **Protect from Sybil Attacks**: We need to implement a way to protect the platform better from Sybil and other attacks. +- **Protect from Sybil Attacks and improved Authentication**: We need to implement a way to protect the platform better from Sybil and other attacks. + +## Future Features + +We also got some additional features we thought about and couldn't implement yet. + +- **Voting Timeframes**: Users creating a new vote can decide whether there's a start and end date for the votes. +- **Multiple Options**: Users can also create votes where multiple options can be selected. # Contributing 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 3727182..21bbd6a 100644 --- a/apps/frontend/src/app/features/voting/create/create.component.html +++ b/apps/frontend/src/app/features/voting/create/create.component.html @@ -67,15 +67,18 @@ > Title* + @if (voteForm.controls['title'].value.length > 70) { 80, + }" id="title-counter" - >{{ voteForm.controls['title'].value.length }}/50 + {{ voteForm.controls['title'].value.length }}/80 + + } + @if (voteForm.hasError('duplicate', ['title', 'description'])) { +
+

+ Title and description must be unique +

+
+ }
+ @if (voteForm.controls['description'].value.length > 340) { {{ voteForm.controls['description'].value.length }}/150 + {{ voteForm.controls['description'].value.length }}/350 + + }
- @if (titleDescriptionDuplicate){ -

- title and description must be unique -

- }
@@ -132,18 +140,20 @@ @for (option of options.controls; track $index) {
+ @if (voteForm.value.options[$index].option.length > 45) { - {{ voteForm.value.options[$index].option.length }}/32 + {{ voteForm.value.options[$index].option.length }}/50 + + }
- } @if (optionsDuplicate){ -

- the options must be unique -

+ } @if (voteForm.hasError('duplicate', ['options'])) { +
+

+ The options must be unique +

+
+ } @if (voteForm.hasError('maxOptionLength', ['options'])) { +
+

+ Each option must be less than 50 characters. +

+
+ } @if (options.controls.length < 5) { + }
- @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 250a688..81f7fff 100644 --- a/apps/frontend/src/app/features/voting/create/create.component.ts +++ b/apps/frontend/src/app/features/voting/create/create.component.ts @@ -6,16 +6,22 @@ import { ReactiveFormsModule, Validators, } from '@angular/forms' -import { NgClass } from '@angular/common' -import { CreateVoteService } from '../../../core/stellar/createVote.service' -import { LoadingComponent } from '../../../shared/feedback/loading/loading.component' -import { ErrorComponent } from '../../../shared/feedback/error/error.component' -import { SuccessComponent } from '../../../shared/feedback/success/success.component' + import { v4 as uuidv4 } from 'uuid' import { BaseVoteConfig } from '../../../types/vote.types' import { ConfirmReloadService } from '../../../shared/services/confirm-reload/confirm-reload.service' import { CanComponentDeactivate } from '../../../types/can-deactivate.interfaces' import { VoteConfigService } from '../../../core/vote-transaction.service' +import { CreateVoteService } from '../../../core/stellar/createVote.service' +import { + maxOptionLengthValidator, + noDuplicateInOptions, + noDuplicateInTitleAndDescription, +} from '../../../utils/create-vote-validators.util' +import { NgClass } from '@angular/common' +import { ErrorComponent } from '../../../shared/feedback/error/error.component' +import { SuccessComponent } from '../../../shared/feedback/success/success.component' +import { LoadingComponent } from '../../../shared/feedback/loading/loading.component' import { ShareComponent } from './share/share.component' @Component({ @@ -24,12 +30,12 @@ import { ShareComponent } from './share/share.component' styleUrls: ['./create.component.css'], standalone: true, imports: [ - ReactiveFormsModule, NgClass, - LoadingComponent, ErrorComponent, SuccessComponent, + LoadingComponent, ShareComponent, + ReactiveFormsModule, ], }) export class CreateComponent @@ -42,45 +48,46 @@ export class CreateComponent public successMessage = '' private baseVoteConfig!: BaseVoteConfig - protected titleDescriptionDuplicate = false - protected optionsDuplicate = false - constructor( private fb: FormBuilder, private createVoteService: CreateVoteService, private confirmReloadService: ConfirmReloadService, private voteConfigService: VoteConfigService, ) { - this.voteForm = this.fb.group({ - id: ['', Validators.required], - title: [ - '', - [ - Validators.required, - Validators.minLength(1), - Validators.maxLength(50), - this.noDuplicateInTitleAndDescription, + this.voteForm = this.fb.group( + { + id: ['', Validators.required], + title: [ + '', + [ + Validators.required, + Validators.minLength(1), + Validators.maxLength(80), + ], ], - ], - description: [ - '', - [ - Validators.required, - Validators.minLength(1), - Validators.maxLength(150), - this.noDuplicateInTitleAndDescription, + description: [ + '', + [ + Validators.required, + Validators.minLength(1), + Validators.maxLength(350), + ], ], - ], - options: this.fb.array( - [], - [ - Validators.required, - Validators.minLength(2), - Validators.maxLength(5), - this.noDuplicateInOptions, - ], - ), - }) + options: this.fb.array( + [], + [ + Validators.required, + Validators.minLength(2), + Validators.maxLength(5), + noDuplicateInOptions(), + maxOptionLengthValidator(50), + ], + ), + }, + { + validators: noDuplicateInTitleAndDescription('title', 'description'), + }, + ) } public get options(): FormArray { @@ -121,6 +128,8 @@ export class CreateComponent } public async onSubmit(): Promise { + if (this.voteForm.invalid) return + this.isLoading = true this.hasError = false this.errorMessage = '' @@ -185,36 +194,4 @@ export class CreateComponent private createVoteId(): string { return uuidv4() } - - private noDuplicateInOptions = ( - control: FormArray, - ): { [key: string]: boolean } | null => { - const values = control.value.map((opt: { option: string }) => opt.option) - - if ( - values.length === new Set(values).size || - values.some((element: string) => element === '') - ) { - this.optionsDuplicate = false - return null - } else { - this.optionsDuplicate = true - return { duplicate: true } - } - } - - protected noDuplicateInTitleAndDescription = (): { - [key: string]: boolean - } | null => { - const title = this.voteForm?.controls['title'].value - const description = this.voteForm?.controls['description'].value - - if (title !== description || !title || !description) { - this.titleDescriptionDuplicate = false - return null - } else { - this.titleDescriptionDuplicate = true - return { duplicate: true } - } - } } diff --git a/apps/frontend/src/app/utils/create-vote-validators.util.ts b/apps/frontend/src/app/utils/create-vote-validators.util.ts new file mode 100644 index 0000000..6aa8b7a --- /dev/null +++ b/apps/frontend/src/app/utils/create-vote-validators.util.ts @@ -0,0 +1,43 @@ +import { + AbstractControl, + FormArray, + ValidationErrors, + ValidatorFn, +} from '@angular/forms' + +export function noDuplicateInOptions(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const formArray = control as FormArray + const values = formArray.value.map((opt: { option: string }) => opt.option) + + const hasDuplicate = values.length !== new Set(values).size + + return hasDuplicate ? { duplicate: true } : null + } +} + +export function maxOptionLengthValidator(maxLength: number = 32): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const formArray = control as FormArray + const hasLongOption = formArray.value.some( + (opt: { option: string }) => opt.option.length > maxLength, + ) + + return hasLongOption ? { maxOptionLength: true } : null + } +} + +export function noDuplicateInTitleAndDescription( + titleControlName: string, + descriptionControlName: string, +): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const formGroup = control as AbstractControl + const title = formGroup.get(titleControlName)?.value + const description = formGroup.get(descriptionControlName)?.value + + const isDuplicate = title === description && title && description + + return isDuplicate ? { duplicate: true } : null + } +}