-
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.
- Loading branch information
Showing
4 changed files
with
357 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { Button } from '@chakra-ui/button' | ||
import { Box, Text } from '@chakra-ui/layout' | ||
import { ModalBody, ModalCloseButton, ModalFooter, ModalHeader } from '@chakra-ui/modal' | ||
import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system' | ||
import { useClient } from '@vocdoni/react-providers' | ||
import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' | ||
import { FieldValues } from 'react-hook-form' | ||
import { useConfirm } from '@vocdoni/chakra-components' | ||
import { ElectionStateStorage } from '~components/Process/MultiElectionContext' | ||
|
||
export type MultiElectionConfirmationProps = { | ||
answers: Record<string, FieldValues> | ||
elections: ElectionStateStorage | ||
} | ||
|
||
export const MultiElectionConfirmation = ({ answers, elections, ...rest }: MultiElectionConfirmationProps) => { | ||
const mstyles = useMultiStyleConfig('ConfirmModal') | ||
const styles = useMultiStyleConfig('QuestionsConfirmation', rest) | ||
const { cancel, proceed } = useConfirm() | ||
const props = omitThemingProps(rest) | ||
const { localize } = useClient() | ||
return ( | ||
<> | ||
<ModalHeader sx={mstyles.header}>{localize('confirm.title')}</ModalHeader> | ||
<ModalCloseButton sx={mstyles.close} /> | ||
<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}> | ||
<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}> | ||
<chakra.div __css={styles.title}>{election.title.default}</chakra.div> | ||
<chakra.div __css={styles.answer}>{localize('vote.not_able_to_vote')}</chakra.div> | ||
</chakra.div> | ||
) | ||
return ( | ||
<Box key={election.id} {...props} sx={styles.box}> | ||
{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)) | ||
return ( | ||
<chakra.div key={k} __css={styles.question}> | ||
<chakra.div __css={styles.title}>{q.title.default}</chakra.div> | ||
<chakra.div __css={styles.answer}>{choice?.title.default}</chakra.div> | ||
</chakra.div> | ||
) | ||
} | ||
const choices = answers[election.id][0] | ||
.map((a: string) => | ||
q.choices[Number(a)] ? q.choices[Number(a)].title.default : localize('vote.abstain') | ||
) | ||
.map((a: string) => ( | ||
<span> | ||
- {a} | ||
<br /> | ||
</span> | ||
)) | ||
|
||
return ( | ||
<chakra.div key={k} __css={styles.question}> | ||
<chakra.div __css={styles.title}>{q.title.default}</chakra.div> | ||
<chakra.div __css={styles.answer}>{choices}</chakra.div> | ||
</chakra.div> | ||
) | ||
})} | ||
</Box> | ||
) | ||
})} | ||
</ModalBody> | ||
<ModalFooter sx={mstyles.footer}> | ||
<Button onClick={cancel!} variant='ghost' sx={mstyles.cancel}> | ||
{localize('confirm.cancel')} | ||
</Button> | ||
<Button onClick={proceed!} sx={mstyles.confirm}> | ||
{localize('confirm.confirm')} | ||
</Button> | ||
</ModalFooter> | ||
</> | ||
) | ||
} |
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 |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import React, { createContext, FC, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' | ||
import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' | ||
import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk' | ||
import { Wallet } from '@ethersproject/wallet' | ||
import { useElection, ElectionState } from '@vocdoni/react-providers' | ||
import { MultiElectionConfirmation } from './MultiElectionConfirmation' | ||
import { useConfirm, getVotePackage } from '@vocdoni/chakra-components' | ||
|
||
export type MultiElectionFormContextState = { | ||
fmethods: UseFormReturn<any> | ||
} & ReturnType<typeof useMultiElectionsProvider> | ||
|
||
export const MultiElectionsContext = createContext<MultiElectionFormContextState | undefined>(undefined) | ||
|
||
export type MultiElectionsProviderProps = { | ||
renderWith: RenderWith[] | ||
rootClient: VocdoniSDKClient | ||
confirmContents?: (elections: ElectionStateStorage, answers: Record<string, FieldValues>) => ReactNode | ||
} | ||
|
||
export type RenderWith = { | ||
id: string | ||
} | ||
|
||
export const MultiElectionsProvider: FC<PropsWithChildren<MultiElectionsProviderProps>> = ({ children, ...props }) => { | ||
const fmethods = useForm() | ||
|
||
const multiElections = useMultiElectionsProvider({ fmethods, ...props }) | ||
return ( | ||
<FormProvider {...fmethods}> | ||
<MultiElectionsContext.Provider value={{ fmethods, ...multiElections }}> | ||
{children} | ||
</MultiElectionsContext.Provider> | ||
</FormProvider> | ||
) | ||
} | ||
|
||
export type SubElectionState = { election: PublishedElection } & Pick<ElectionState, 'vote' | 'isAbleToVote' | 'voted'> | ||
export type ElectionStateStorage = Record<string, SubElectionState> | ||
|
||
const useMultiElectionsProvider = ({ | ||
fmethods, | ||
renderWith, | ||
rootClient, | ||
confirmContents, | ||
}: { fmethods: UseFormReturn } & MultiElectionsProviderProps) => { | ||
const { confirm } = useConfirm() | ||
const { client } = useElection() | ||
// 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 addElection = (electionState: SubElectionState) => { | ||
setElectionsStates((prev) => ({ | ||
...prev, | ||
[(electionState.election as PublishedElection).id]: electionState, | ||
})) | ||
} | ||
|
||
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 }]) => { | ||
if (!(election instanceof PublishedElection) || !values[election.id]) return Promise.resolve() | ||
const votePackage = getVotePackage(election, values[election.id]) | ||
return vote(votePackage) | ||
}) | ||
return Promise.all(votingList).finally(() => setVoting(false)) | ||
} | ||
|
||
// 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]) | ||
|
||
const voted = useMemo( | ||
() => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null), | ||
[electionsStates] | ||
) | ||
const isAbleToVote = useMemo( | ||
() => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), | ||
[electionsStates] | ||
) | ||
|
||
return { | ||
voting, | ||
voteAll, | ||
renderWith, | ||
rootClient, | ||
elections: electionsStates, | ||
addElection, | ||
isAbleToVote, | ||
voted, | ||
} | ||
} | ||
|
||
export const useMultiElections = () => { | ||
const context = useContext(MultiElectionsContext) | ||
if (!context) { | ||
throw new Error('useMultiElections must be used within an MultiElectionsProvider') | ||
} | ||
return context | ||
} |
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 |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { | ||
MultiElectionsProvider, | ||
MultiElectionsProviderProps, | ||
SubElectionState, | ||
useMultiElections, | ||
} from './MultiElectionContext' | ||
import { ElectionProvider, useElection } from '@vocdoni/react-providers' | ||
import { ComponentType, useEffect, useMemo, useState } from 'react' | ||
import { PublishedElection } from '@vocdoni/sdk' | ||
import { ButtonProps } from '@chakra-ui/button' | ||
import { | ||
ElectionQuestionsFormProps, | ||
ElectionQuestion, | ||
DefaultElectionFormId, | ||
VoteButtonLogic, | ||
} from '@vocdoni/chakra-components' | ||
|
||
export type MultiElectionQuestionsFormProps = { ConnectButton?: ComponentType } & ElectionQuestionsFormProps | ||
|
||
export const MultiElectionVoteButton = (props: ButtonProps) => { | ||
const { isAbleToVote, voting, voted } = useMultiElections() | ||
const election = useElection() // use Root election information | ||
|
||
return ( | ||
<VoteButtonLogic | ||
electionState={{ ...election, voted, loading: { ...election.loading, voting }, isAbleToVote }} | ||
{...props} | ||
/> | ||
) | ||
} | ||
|
||
export const MultiElectionQuestionsForm = ({ | ||
formId, | ||
onInvalid, | ||
ConnectButton, | ||
...props | ||
}: MultiElectionQuestionsFormProps) => { | ||
const { voteAll, fmethods, renderWith, elections, addElection } = useMultiElections() | ||
|
||
return ( | ||
<form onSubmit={fmethods.handleSubmit(voteAll, onInvalid)} id={formId ?? DefaultElectionFormId}> | ||
{/*<ElectionQuestion {...props} />*/} | ||
{renderWith.length > 0 && ( | ||
<> | ||
{renderWith.map(({ id }) => ( | ||
<ElectionProvider key={id} ConnectButton={ConnectButton} id={id} fetchCensus> | ||
<SubElectionQuestions {...props} /> | ||
</ElectionProvider> | ||
))} | ||
</> | ||
)} | ||
</form> | ||
) | ||
} | ||
|
||
const SubElectionQuestions = (props: Omit<MultiElectionQuestionsFormProps, 'ConnectButton'>) => { | ||
const { rootClient, addElection, elections } = useMultiElections() | ||
const { election, setClient, vote, client, connected, clearClient, isAbleToVote, voted } = useElection() | ||
|
||
const subElectionState: SubElectionState | null = useMemo(() => { | ||
if (!election || !(election instanceof PublishedElection)) return null | ||
return { | ||
vote, | ||
election, | ||
isAbleToVote, | ||
voted, | ||
} | ||
}, [vote, election, isAbleToVote, voted]) | ||
|
||
// clear session of local context when login out | ||
useEffect(() => { | ||
if (connected) return | ||
clearClient() | ||
}, [connected]) | ||
|
||
// ensure the client is set to the root one | ||
useEffect(() => { | ||
setClient(rootClient) | ||
}, [rootClient, election]) | ||
|
||
// Update election state cache | ||
useEffect(() => { | ||
if (!subElectionState || !subElectionState.election) return | ||
const actualState = elections[subElectionState.election.id] | ||
if (subElectionState.vote === actualState?.vote || subElectionState.isAbleToVote === actualState?.isAbleToVote) { | ||
return | ||
} | ||
addElection(subElectionState) | ||
|
||
// } | ||
// ;(async () => { | ||
// if ( | ||
// election && | ||
// election instanceof PublishedElection | ||
// // client?.wallet && | ||
// // typeof client.wallet.getAddress === 'function' | ||
// ) { | ||
// // Store the election if wallet contain address | ||
// // const address = await client.wallet.getAddress() | ||
// // if (walletAddress === address && elections[election.id]) return | ||
// // setWalletAddress(address) | ||
// addElection({ election, vote, isAbleToVote, voted }) | ||
// } | ||
// })() | ||
}, [subElectionState, elections, election]) | ||
|
||
return <ElectionQuestion {...props} /> | ||
} |