Skip to content

Commit

Permalink
fix: improve create vote form validation
Browse files Browse the repository at this point in the history
  • Loading branch information
danieljancar committed Aug 10, 2024
1 parent 1114749 commit 9860393
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 120 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -77,15 +80,22 @@ 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

As this project was developed in a short amount of time, there are some known issues that we would like to call out and address in the future:

- **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

Expand Down
110 changes: 63 additions & 47 deletions apps/frontend/src/app/features/voting/create/create.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,18 @@
>
Title*
</label>
@if (voteForm.controls['title'].value.length > 70) {
<span
class="text-sm leading-6"
[ngClass]="{
'text-gray-500': voteForm.controls['title'].value.length <= 150,
'text-red-600': voteForm.controls['title'].value.length > 150
}"
'text-gray-500': voteForm.controls['title'].value.length <= 75,
'text-red-600': voteForm.controls['title'].value.length > 80,
}"
id="title-counter"
>{{ voteForm.controls['title'].value.length }}/50</span
>
{{ voteForm.controls['title'].value.length }}/80
</span>
}
</div>

<input
Expand All @@ -87,27 +90,37 @@
required
type="text"
/>
@if (voteForm.hasError('duplicate', ['title', 'description'])) {
<div>
<p class="mt-2 text-sm text-red-600" id="description-error">
Title and description must be unique
</p>
</div>
}
</div>

<div class="space-y-2">
<div class="flex justify-between">
<label
class="block text-sm font-medium leading-5 text-gray-700"
for="title"
for="description"
>
Description*
</label>
@if (voteForm.controls['description'].value.length > 340) {
<span
class="text-sm leading-6"
[ngClass]="{
'text-gray-500':
voteForm.controls['description'].value.length <= 150,
voteForm.controls['description'].value.length <= 340,
'text-red-600':
voteForm.controls['description'].value.length > 150
voteForm.controls['description'].value.length > 350
}"
id="description-count"
>{{ voteForm.controls['description'].value.length }}/150</span
id="description-counter"
>
{{ voteForm.controls['description'].value.length }}/350
</span>
}
</div>
<textarea
class="block w-full rounded-none rounded-l-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
Expand All @@ -118,11 +131,6 @@
required
rows="3"
></textarea>
@if (titleDescriptionDuplicate){
<p class="mt-2 text-sm text-red-600" id="description-error">
title and description must be unique
</p>
}
</div>

<div formArrayName="options">
Expand All @@ -132,18 +140,20 @@
@for (option of options.controls; track $index) {
<div [formGroupName]="$index" class="space-y-2">
<div class="flex justify-end">
@if (voteForm.value.options[$index].option.length > 45) {
<span
[ngClass]="{
'text-gray-500':
voteForm.value.options[$index].option.length >= 0,
voteForm.value.options[$index].option.length <= 45,
'text-red-600':
voteForm.value.options[$index].option.length > 32
voteForm.value.options[$index].option.length > 50
}"
class="text-sm leading-6 text-gray-500"
id="{{ $index }}-count"
>
{{ voteForm.value.options[$index].option.length }}/32</span
>
{{ voteForm.value.options[$index].option.length }}/50
</span>
}
</div>
<div class="mt-2 flex rounded-md shadow-sm">
<input
Expand Down Expand Up @@ -179,38 +189,44 @@
}
</div>
</div>
} @if (optionsDuplicate){
<p class="mt-2 text-sm text-red-600" id="options-error">
the options must be unique
</p>
} @if (voteForm.hasError('duplicate', ['options'])) {
<div>
<p class="mt-2 text-sm text-red-600" id="options-error">
The options must be unique
</p>
</div>
} @if (voteForm.hasError('maxOptionLength', ['options'])) {
<div>
<p class="mt-2 text-sm text-red-600" id="options-length-error">
Each option must be less than 50 characters.
</p>
</div>
} @if (options.controls.length < 5) {
<button
(click)="addOption()"
aria-label="Add Option"
class="mt-2 inline-flex items-center px-3 py-1 text-sm font-medium leading-5 text-green-700 transition duration-150 ease-in-out bg-white border border-green-300 rounded-md shadow-sm hover:bg-green-50 focus:outline-none focus:shadow-outline-green focus:bg-green-100 active:bg-green-100"
type="button"
>
Add Option
</button>
}
</div>

@if (options.controls.length < 5) {
<button
(click)="addOption()"
aria-label="Add Option"
class="mt-2 inline-flex items-center px-3 py-1 text-sm font-medium leading-5 text-green-700 transition duration-150 ease-in-out bg-white border border-green-300 rounded-md shadow-sm hover:bg-green-50 focus:outline-none focus:shadow-outline-green focus:bg-green-100 active:bg-green-100"
type="button"
>
Add Option
</button>
}
</div>

<div class="flex justify-end">
<button
[disabled]="voteForm.invalid"
[ngClass]="{
'bg-gray-700 hover:bg-gray-700': voteForm.invalid,
'bg-indigo-600 hover:bg-indigo-700': voteForm.valid
}"
aria-label="Create Vote"
class="w-full inline-flex items-center justify-center px-4 py-2 text-sm font-semibold text-white border border-transparent rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
type="submit"
>
Create Vote
</button>
<div class="flex justify-end">
<button
[disabled]="voteForm.invalid"
[ngClass]="{
'bg-gray-700 hover:bg-gray-700': voteForm.invalid,
'bg-indigo-600 hover:bg-indigo-700': voteForm.valid
}"
aria-label="Create Vote"
class="w-full inline-flex items-center justify-center px-4 py-2 text-sm font-semibold text-white border border-transparent rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
type="submit"
>
Create Vote
</button>
</div>
</div>
</div>
</form>
Expand Down
117 changes: 47 additions & 70 deletions apps/frontend/src/app/features/voting/create/create.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -121,6 +128,8 @@ export class CreateComponent
}

public async onSubmit(): Promise<void> {
if (this.voteForm.invalid) return

this.isLoading = true
this.hasError = false
this.errorMessage = ''
Expand Down Expand Up @@ -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 }
}
}
}
Loading

0 comments on commit 9860393

Please sign in to comment.