Skip to content

Commit

Permalink
Merge Multielection with the normal form
Browse files Browse the repository at this point in the history
  • Loading branch information
selankon committed Oct 17, 2024
1 parent c745e91 commit ef03ef0
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 317 deletions.
177 changes: 123 additions & 54 deletions packages/chakra-components/src/components/Election/Questions/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Wallet } from '@ethersproject/wallet'
import { useElection } from '@vocdoni/react-providers'
import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk'
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect } from 'react'
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react'
import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form'
import { useConfirm } from '../../layout'
import { QuestionsConfirmation } from './Confirmation'
import { MultiElectionConfirmation } from './MultiElectionConfirmation'
import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions'

export const DefaultElectionFormId = 'election-questions'

export type QuestionsFormContextState = {
fmethods: UseFormReturn<any>
vote: (values: FieldValues) => Promise<false | void>
}
} & SpecificFormProviderProps &
ReturnType<typeof useMultiElectionsProvider>

const QuestionsFormContext = createContext<QuestionsFormContextState | undefined>(undefined)

Expand All @@ -24,66 +25,33 @@ export const useQuestionsForm = () => {
}

export type QuestionsFormProviderProps = {
confirmContents?: (election: PublishedElection, answers: FieldValues) => ReactNode
confirmContents?: (elections: ElectionStateStorage, answers: Record<string, FieldValues>) => ReactNode
}

export const QuestionsFormProvider: React.FC<PropsWithChildren<QuestionsFormProviderProps>> = ({
confirmContents,
children,
}) => {
const fmethods = useForm()
const { confirm } = useConfirm()
const { election, client, vote: bvote } = useElection()

const vote = async (values: Record<string, FieldValues>) => {
if (!election || !(election instanceof PublishedElection)) {
console.warn('vote attempt with no valid election defined')
return false
}

const electionChoices = values[election.id]

if (
client.wallet instanceof Wallet &&
!(await confirm(
typeof confirmContents === 'function' ? (
confirmContents(election, electionChoices)
) : (
<QuestionsConfirmation election={election} answers={electionChoices} />
)
))
) {
return false
}

const votePackage = getVoteBallot(election, electionChoices)

return bvote(votePackage)
}

// reset form if account gets disconnected
useEffect(() => {
if (
typeof client.wallet !== 'undefined' ||
!election ||
!(election instanceof PublishedElection) ||
!election?.questions
)
return
// Props that must not be shared with ElectionQuestionsProps
export type SpecificFormProviderProps = {
renderWith?: RenderWith[]
validate?: SubmitFormValidation
}

fmethods.reset({
...election.questions.reduce((acc, question, index) => ({ ...acc, [index]: '' }), {}),
})
}, [client, election, fmethods])
export const QuestionsFormProvider: React.FC<
PropsWithChildren<QuestionsFormProviderProps & SpecificFormProviderProps>
> = ({ children, ...props }) => {
const fmethods = useForm()
const multiElections = useMultiElectionsProvider({ fmethods, ...props })

return (
<FormProvider {...fmethods}>
<QuestionsFormContext.Provider value={{ fmethods, vote }}>{children}</QuestionsFormContext.Provider>
<QuestionsFormContext.Provider
value={{ fmethods, renderWith: props.renderWith, validate: props.validate, ...multiElections }}
>
{children}
</QuestionsFormContext.Provider>
</FormProvider>
)
}

export const getVoteBallot = (election: PublishedElection, choices: FieldValues) => {
export const constructVoteBallot = (election: PublishedElection, choices: FieldValues) => {
let results: number[] = []
switch (election.resultsType.name) {
case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION:
Expand Down Expand Up @@ -115,3 +83,104 @@ export const getVoteBallot = (election: PublishedElection, choices: FieldValues)
}
return results
}

const useMultiElectionsProvider = ({
fmethods,
confirmContents,
}: { fmethods: UseFormReturn } & QuestionsFormProviderProps) => {
const { confirm } = useConfirm()
const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, vote } = useElection() // Root Election
// State to store on memory the loaded elections to pass it into confirm modal to show the info
const [electionsStates, setElectionsStates] = useState<ElectionStateStorage>({})
const [voting, setVoting] = useState<boolean>(false)

const voted = useMemo(
() => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null),
[electionsStates]
)

const isAbleToVote = useMemo(
() => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote),
[electionsStates]
)

// Add an election to the storage
const addElection = (electionState: SubElectionState) => {
setElectionsStates((prev) => ({
...prev,
[(electionState.election as PublishedElection).id]: electionState,
}))
}

// Root election state to be added to the state storage
const rootElectionState: SubElectionState | null = useMemo(() => {
if (!election || !(election instanceof PublishedElection)) return null
return {
vote,
election,
isAbleToVote: rootIsAbleToVote,
voted: rootVoted,
}
}, [vote, election, rootIsAbleToVote, rootVoted])

// reset form if account gets disconnected
useEffect(() => {
if (typeof client.wallet !== 'undefined') return

setElectionsStates({})
fmethods.reset({
...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}),
})
}, [client, electionsStates, fmethods])

// Add the root election to the state to elections cache
useEffect(() => {
if (!rootElectionState || !rootElectionState.election) return
const actualState = electionsStates[rootElectionState.election.id]
if (rootElectionState.vote === actualState?.vote || rootElectionState.isAbleToVote === actualState?.isAbleToVote) {
return
}
addElection(rootElectionState)
}, [rootElectionState, electionsStates, election])

const voteAll = async (values: Record<string, FieldValues>) => {
if (!electionsStates || Object.keys(electionsStates).length === 0) {
console.warn('vote attempt with no valid elections not defined')
return false
}

if (
client.wallet instanceof Wallet &&
!(await confirm(
typeof confirmContents === 'function' ? (
confirmContents(electionsStates, values)
) : (
<MultiElectionConfirmation elections={electionsStates} answers={values} />
)
))
) {
return false
}

setVoting(true)

const votingList = Object.entries(electionsStates).map(([key, { election, vote, voted, isAbleToVote }]) => {
if (!(election instanceof PublishedElection) || !values[election.id] || !isAbleToVote) {
return Promise.resolve()
}
const votePackage = constructVoteBallot(election, values[election.id])
return vote(votePackage)
})
return Promise.all(votingList).finally(() => setVoting(false))
}

return {
voting,
voteAll,
rootClient: client,
elections: electionsStates,
addElection,
isAbleToVote,
voted,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system
import { useClient } from '@vocdoni/react-providers'
import { ElectionResultsTypeNames } from '@vocdoni/sdk'
import { FieldValues } from 'react-hook-form'
import { ElectionStateStorage } from './MultiElectionContext'
import { useConfirm } from '../../layout'
import { ElectionStateStorage } from './Questions'

export type MultiElectionConfirmationProps = {
answers: Record<string, FieldValues>
elections: ElectionStateStorage
}

// todo(kon): refactor this to merge it with the current Confirmation modal
export const MultiElectionConfirmation = ({ answers, elections, ...rest }: MultiElectionConfirmationProps) => {
const mstyles = useMultiStyleConfig('ConfirmModal')
const styles = useMultiStyleConfig('QuestionsConfirmation', rest)
Expand All @@ -26,13 +27,13 @@ export const MultiElectionConfirmation = ({ answers, elections, ...rest }: Multi
<ModalBody sx={mstyles.body}>
<Text sx={styles.description}>{localize('vote.confirm')}</Text>
{Object.values(elections).map(({ election, voted, isAbleToVote }) => {
if (voted)
return (
<chakra.div __css={styles.question} key={election.id}>
<chakra.div __css={styles.title}>{election.title.default}</chakra.div>
<chakra.div __css={styles.answer}>{localize('vote.already_voted')}</chakra.div>
</chakra.div>
)
// if (voted)
// return (
// <chakra.div __css={styles.question} key={election.id}>
// <chakra.div __css={styles.title}>{election.title.default}</chakra.div>
// <chakra.div __css={styles.answer}>{localize('vote.already_voted')}</chakra.div>
// </chakra.div>
// )
if (!isAbleToVote)
return (
<chakra.div __css={styles.question} key={election.id}>
Expand All @@ -42,6 +43,7 @@ export const MultiElectionConfirmation = ({ answers, elections, ...rest }: Multi
)
return (
<Box key={election.id} {...props} sx={styles.box}>
{/*todo(kon): refactor to add election title and if already voted but can overwrite*/}
{election.questions.map((q, k) => {
if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) {
const choice = q.choices.find((v) => v.value === parseInt(answers[election.id][k.toString()], 10))
Expand Down

This file was deleted.

Loading

0 comments on commit ef03ef0

Please sign in to comment.