Skip to content

Commit

Permalink
Implement create process form logic (#774)
Browse files Browse the repository at this point in the history
This PR implements some new necessary logic in order to follow new saas requirements for process creation. Related:

vocdoni/interoperability#210
vocdoni/interoperability#229

- Adds a footer on process creation
- Adds a store draft button
- Implement new steps when is saas
- Add saas extra features on the form 
- Mock `useAccountPlan`, simulating a call that gives for an organization which features a plan has activated.
- Add some minimum styles to try it easier

## Important!

This PR **does not** implement:

- New styles
- Some buttons logics (for example save draft or upgrade plan)
- New modals
- Logic to store the election on the saas backend (this need some meetings). For now, the SDK and the remote signer are able to create an election on the backend as before.
  • Loading branch information
selankon authored Oct 3, 2024
1 parent d90b806 commit 7d305c7
Show file tree
Hide file tree
Showing 19 changed files with 619 additions and 25 deletions.
74 changes: 74 additions & 0 deletions src/components/AccountSaas/useAccountPlan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { VotingType } from '~components/ProcessCreate/Questions/useVotingType'
import { UnimplementedVotingType } from '~components/ProcessCreate/Questions/useUnimplementedVotingType'

type PlanType = 'free' | 'pro' | 'custom'

export type FeaturesKeys =
| 'anonymous'
| 'secretUntilTheEnd'
| 'overwrite'
| 'personalization'
| 'emailReminder'
| 'smsNotification'
| 'whiteLabel'
| 'liveStreaming'
export type SaasVotingTypesKeys = VotingType & UnimplementedVotingType

type SaasOrganizationInfo = {
memberships: number
subOrgs: number
maxProcesses: number
max_census_size: number
customURL: boolean
}

type AccountPlanTypeResponse = {
plan: PlanType
stripePlanId: string
organization: SaasOrganizationInfo
votingTypes: Record<SaasVotingTypesKeys, boolean>
features: Record<FeaturesKeys, boolean>
}

const accountPlanMock: AccountPlanTypeResponse = {
plan: 'pro',
stripePlanId: 'plan_xyz123', // Stripe plan ID for payment and plan management
organization: {
memberships: 5, // Maximum number of members or admins
subOrgs: 3, // Maximum number of sub-organizations
maxProcesses: 10, // Maximum number of voting processes
max_census_size: 10000, // Maximum number of voters (census size) for this plan
customURL: true, // Whether a custom URL for the voting page is allowed
},
votingTypes: {
single: true, // Simple single-choice voting allowed
multiple: true, // Multiple-choice voting allowed
approval: true, // Approval voting allowed
cumulative: false, // Cumulative voting not allowed
ranked: false, // Ranked voting not allowed
weighted: false, // Weighted voting not allowed
},
features: {
personalization: true, // Voting page customization allowed
emailReminder: true, // Email reminders allowed
smsNotification: true, // SMS notifications allowed
whiteLabel: true, // White-label voting page allowed
liveStreaming: false, // Live results streaming not allowed
anonymous: true,
secretUntilTheEnd: true,
overwrite: true,
// ... Other feature controls
},
}

export const useAccountPlan = (options?: Omit<UseQueryOptions<AccountPlanTypeResponse>, 'queryKey' | 'queryFn'>) => {
return useQuery({
queryKey: ['account', 'plan'],
queryFn: async () => {
// Simulate an API call
return accountPlanMock
},
...options,
})
}
2 changes: 0 additions & 2 deletions src/components/ProcessCreate/Census/TypeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
CensusType,
CensusTypeCsp,
CensusTypeGitcoin,
CensusTypes,
CensusTypeSpreadsheet,
CensusTypeToken,
CensusTypeWeb3,
Expand All @@ -16,7 +15,6 @@ export const useCensusTypes = (): GenericFeatureObject<CensusType> => {
const { t } = useTranslation()

return {
list: CensusTypes,
defined: import.meta.env.features.census as CensusType[],
details: {
[CensusTypeSpreadsheet]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const UnimplementedCensusTypes = [
export const useUnimplementedCensusTypes = (): GenericFeatureObject<UnimplementedCensusType> => {
const { t } = useTranslation()
return {
list: UnimplementedCensusTypes,
defined: import.meta.env.features.unimplemented_census as UnimplementedCensusType[],
details: {
[CensusTypePhone]: {
Expand Down
83 changes: 83 additions & 0 deletions src/components/ProcessCreate/Questions/useSaasVotingType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useTranslation } from 'react-i18next'
import { GenericFeatureObject, GenericFeatureObjectProps } from '~components/ProcessCreate/Steps/TabsPage'
import { SaasVotingTypesKeys, useAccountPlan } from '~components/AccountSaas/useAccountPlan'
import { GiChoice } from 'react-icons/gi'
import SingleChoice from '~components/ProcessCreate/Questions/SingleChoice'
import { useMemo } from 'react'

const useVotingTypesTranslations = (): Record<SaasVotingTypesKeys, GenericFeatureObjectProps> => {
const { t } = useTranslation()
return useMemo(
() => ({
single: {
description: t('single', { defaultValue: 'single' }),
title: t('single', { defaultValue: 'single' }),
icon: GiChoice,
component: SingleChoice,
},
multiple: {
description: t('multiple', { defaultValue: 'multiple' }),
title: t('multiple', { defaultValue: 'multiple' }),
icon: GiChoice,
component: SingleChoice,
},
approval: {
description: t('approval', { defaultValue: 'approval' }),
title: t('approval', { defaultValue: 'approval' }),
icon: GiChoice,
component: SingleChoice,
},
cumulative: {
description: t('cumulative', { defaultValue: 'cumulative' }),
title: t('cumulative', { defaultValue: 'cumulative' }),
icon: GiChoice,
component: SingleChoice,
},
ranked: {
description: t('ranked', { defaultValue: 'ranked' }),
title: t('ranked', { defaultValue: 'ranked' }),
icon: GiChoice,
component: SingleChoice,
},
weighted: {
description: t('weighted', { defaultValue: 'weighted' }),
title: t('weighted', { defaultValue: 'weighted' }),
icon: GiChoice,
component: SingleChoice,
},
}),
[t]
)
}

export const useSaasVotingType = (): {
inPlan: GenericFeatureObject<Partial<SaasVotingTypesKeys>>
pro: GenericFeatureObject<Partial<SaasVotingTypesKeys>>
} => {
const { data } = useAccountPlan()
const translations = useVotingTypesTranslations()

if (!data) return null
const inPlanDetails = {} as Record<Partial<SaasVotingTypesKeys>, GenericFeatureObjectProps>
const proDetails = {} as Record<Partial<SaasVotingTypesKeys>, GenericFeatureObjectProps>

for (const [key, inPlan] of Object.entries(data.votingTypes)) {
const _key = key as SaasVotingTypesKeys
if (inPlan) {
inPlanDetails[_key] = translations[_key]
continue
}
proDetails[_key] = translations[_key]
}

return {
inPlan: {
defined: Object.keys(inPlanDetails) as Partial<SaasVotingTypesKeys>[],
details: inPlanDetails,
},
pro: {
defined: Object.keys(proDetails) as Partial<SaasVotingTypesKeys>[],
details: proDetails,
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const UnimplementedVotingType = [
export const useUnimplementedVotingType = (): GenericFeatureObject<UnimplementedVotingType> => {
const { t } = useTranslation()
return {
list: UnimplementedVotingType,
defined: import.meta.env.features.unimplemented_voting_type as UnimplementedVotingType[],
details: {
[UnimplementedVotingTypeMulti]: {
Expand Down
1 change: 0 additions & 1 deletion src/components/ProcessCreate/Questions/useVotingType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const VotingTypes = [VotingTypeSingle as VotingType, UnimplementedVotingT
export const useVotingType = (): GenericFeatureObject<VotingType> => {
const { t } = useTranslation()
return {
list: VotingTypes,
defined: import.meta.env.features.voting_type as VotingType[],
details: {
[VotingTypeSingle]: {
Expand Down
24 changes: 24 additions & 0 deletions src/components/ProcessCreate/SaasFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Box, Flex, HStack, Image, Stack, Text } from '@chakra-ui/react'
import vcdLogo from '/assets/logo-classic.svg'
import { Button } from '@vocdoni/chakra-components'
import { useAccountPlan } from '~components/AccountSaas/useAccountPlan'

const SaasFooter = () => {
const { data } = useAccountPlan()
const isCustom = data?.plan === 'custom'
const isFree = data?.plan === 'free'

return (
<Box as='footer' mt='auto'>
<Flex justify={'space-around'} gap={6}>
<Image src={vcdLogo} w='125px' mb='12px' />
<Text fontSize='sm'>Privacy Policy</Text>
<Text fontSize='sm'>[email protected]</Text>
{isFree && <Text fontSize='sm'>$0.00</Text>}
{!isCustom && <Button>UPGRADE TO PREMIUM</Button>}
</Flex>
</Box>
)
}

export default SaasFooter
99 changes: 99 additions & 0 deletions src/components/ProcessCreate/Settings/SaasFeatures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Box, Checkbox, CheckboxProps, Icon, Text } from '@chakra-ui/react'
import { BiCheckDouble } from 'react-icons/bi'
import { IconType } from 'react-icons'
import { FeaturesKeys, useAccountPlan } from '~components/AccountSaas/useAccountPlan'
import { Loading } from '~src/router/SuspenseLoader'
import { useTranslation } from 'react-i18next'
import { useFormContext } from 'react-hook-form'
import { useMemo } from 'react'

const useFeaturesTranslations = (): Record<FeaturesKeys, CheckBoxCardProps> => {
const { t } = useTranslation()
return useMemo(
() => ({
anonymous: {
description: t('anonymous', { defaultValue: 'anonymous' }),
title: t('anonymous', { defaultValue: 'anonymous' }),
boxIcon: BiCheckDouble,
formKey: 'electionType.anonymous',
},
secretUntilTheEnd: {
description: t('secretUntilTheEnd', { defaultValue: 'secretUntilTheEnd' }),
title: t('secretUntilTheEnd', { defaultValue: 'secretUntilTheEnd' }),
boxIcon: BiCheckDouble,
formKey: 'electionType.secretUntilTheEnd',
},
overwrite: {
description: t('overwrite', { defaultValue: 'overwrite' }),
title: t('overwrite', { defaultValue: 'overwrite' }),
boxIcon: BiCheckDouble,
},
personalization: {
description: t('personalization', { defaultValue: 'personalization' }),
title: t('personalization', { defaultValue: 'personalization' }),
boxIcon: BiCheckDouble,
},
emailReminder: {
description: t('emailReminder', { defaultValue: 'emailReminder' }),
title: t('emailReminder', { defaultValue: 'emailReminder' }),
boxIcon: BiCheckDouble,
},
smsNotification: {
description: t('smsNotification', { defaultValue: 'smsNotification' }),
title: t('smsNotification', { defaultValue: 'smsNotification' }),
boxIcon: BiCheckDouble,
},
whiteLabel: {
description: t('whiteLabel', { defaultValue: 'whiteLabel' }),
title: t('whiteLabel', { defaultValue: 'whiteLabel' }),
boxIcon: BiCheckDouble,
},
liveStreaming: {
description: t('liveStreaming', { defaultValue: 'liveStreaming' }),
title: t('liveStreaming', { defaultValue: 'liveStreaming' }),
boxIcon: BiCheckDouble,
},
}),
[t]
)
}
export const SaasFeatures = () => {
const { data, isLoading } = useAccountPlan()
const translations = useFeaturesTranslations()

if (isLoading) return <Loading />
if (!data) return null

return (
<Box>
{Object.entries(data.features).map(([feature, inPlan], i) => {
const card = translations[feature as FeaturesKeys]
if (!card) return null
return <CheckBoxCard key={i} isPro={!inPlan} {...card} formKey={card.formKey ?? `saasFeatures.${feature}`} />
})}
</Box>
)
}

interface CheckBoxCardProps {
title: string
description: string
boxIcon: IconType
isPro?: boolean
formKey?: string
}

const CheckBoxCard = ({ title, description, boxIcon, isPro, formKey, ...rest }: CheckBoxCardProps & CheckboxProps) => {
const { register, watch } = useFormContext()

return (
<Checkbox variant='radiobox' {...register(formKey)} {...rest}>
<Box>
<Icon as={boxIcon} />
<Text>{title}</Text>
</Box>
{isPro && <Text as='span'>Pro</Text>}
<Text>{description}</Text>
</Checkbox>
)
}
2 changes: 1 addition & 1 deletion src/components/ProcessCreate/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Calendar from './Calendar'
const CreateProcessSettings = () => (
<>
<Calendar />
<Advanced />
{!import.meta.env.SAAS_URL && <Advanced />}
</>
)

Expand Down
9 changes: 7 additions & 2 deletions src/components/ProcessCreate/StepForm/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export interface InfoValues {
// dates need to be string to properly reset the values to the inputs
endDate: string
startDate: string
}

export interface ConfigurationValues {
electionType: {
autoStart: boolean
interruptible: boolean
Expand All @@ -26,13 +29,15 @@ export interface InfoValues {
weightedVote: boolean
}

type FormValues = InfoValues & ConfigurationValues

export const Info = () => {
const { form, setForm, next } = useProcessCreationSteps()
const methods = useForm<InfoValues>({
const methods = useForm<FormValues>({
defaultValues: form,
})

const onSubmit: SubmitHandler<InfoValues> = (data) => {
const onSubmit: SubmitHandler<FormValues> = (data) => {
setForm({ ...form, ...data })
next()
}
Expand Down
Loading

2 comments on commit 7d305c7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.