Skip to content

Commit

Permalink
Integrated into process create flow
Browse files Browse the repository at this point in the history
- Had to undo part of the work done by ritmo in the features, since
  the form state was messied with unnecessarily.
- Unified the tabs and checkboxes styles, both using the same
  DetailedBox component styles (since they are identical).
- Modals were integrated in all process options, except for the
  maxCensusSize, which will be in the next commit
  • Loading branch information
elboletaire committed Nov 29, 2024
1 parent 41e3239 commit 23a2655
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 241 deletions.
File renamed without changes.
23 changes: 2 additions & 21 deletions src/components/Auth/Subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useClient } from '@vocdoni/react-providers'
import { dotobject, ensure0x } from '@vocdoni/sdk'
import { ReactNode, useMemo } from 'react'
import { useAuth } from '~components/Auth/useAuth'
import type { Plan } from '~components/Pricing/Plans'
import { ApiEndpoints } from './api'

type PermissionsContextType = {
Expand All @@ -27,27 +28,7 @@ type SubscriptionType = {
subOrgs: number
members: number
}
plan: {
id: number
name: string
stripeID: string
default: boolean
organization: {
memberships: number
subOrgs: number
censusSize: number
}
votingTypes: {
approval: boolean
ranked: boolean
weighted: boolean
}
features: {
personalization: boolean
emailReminder: boolean
smsNotification: boolean
}
}
plan: Plan
}

const [SubscriptionProvider, useSubscription] = createContext<PermissionsContextType>({
Expand Down
28 changes: 28 additions & 0 deletions src/components/Layout/Form/DetailedCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Checkbox, CheckboxProps, Text, useMultiStyleConfig } from '@chakra-ui/react'
import { cloneElement, ReactElement } from 'react'
import { useFormContext } from 'react-hook-form'

export type DetailedBoxProps = CheckboxProps & {
badge?: ReactElement
description?: string
icon?: ReactElement
name: string
title: string
}

export const DetailedBox = ({ icon, badge, title, description, name, ...props }: DetailedBoxProps) => {
const styles = useMultiStyleConfig('DetailedBox', props)
const { register } = useFormContext()

return (
<Checkbox variant='detailed' {...register(name)} {...props} sx={styles.checkbox}>
<Text sx={styles.title}>
{icon && cloneElement(icon, { sx: styles.icon })}
{title}
</Text>
<Text sx={styles.description}>{description}</Text>

{badge && cloneElement(badge, { sx: styles.badge })}
</Checkbox>
)
}
19 changes: 10 additions & 9 deletions src/components/Pricing/Features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@ import { dotobject } from '@vocdoni/sdk'
import { useTranslation } from 'react-i18next'
import type { Plan } from './Plans'

export type FeaturesKeys =
| 'anonymous'
| 'secretUntilTheEnd'
| 'overwrite'
| 'personalization'
| 'emailReminder'
| 'smsNotification'
| 'whiteLabel'
| 'liveStreaming'
export type FeaturesKeys = 'personalization' | 'emailReminder' | 'smsNotification' | 'whiteLabel' | 'liveStreaming'

// Translation keys for the subscription features
export const PlanFeaturesTranslationKeys = {
Expand All @@ -24,6 +16,15 @@ export const PlanFeaturesTranslationKeys = {
'features.smsNotification': 'features.sms_notification',
}

/**
* Checks if the specified feature exists in the plan.
*
* @param plan - The plan object to check.
* @param featurePath - Dot notation path to the feature (e.g., 'organization.memberships').
* @returns boolean - `true` if the feature exists, `false` otherwise.
*/
export const hasFeature = (plan: Plan, featurePath: string) => dotobject(plan, featurePath) !== 'undefined'

/**
* Checks if a given feature exists and meets the required condition in a plan.
*
Expand Down
16 changes: 9 additions & 7 deletions src/components/Pricing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@ export const isFeatureAvailable = (
featurePath: string,
expectedValue?: number | { operator: '===' | '>' | '>=' | '<' | '<='; value: number }
): boolean => {
const featureValue = dotobject(plan, featurePath) // Get the feature value using dot notation
// Get the feature value using dot notation
const featureValue = dotobject(plan, featurePath)

// If the feature doesn't exist, return false
if (typeof featureValue === 'undefined') {
return false // If the feature doesn't exist, return false
}

// If no expected value is provided, return true if the feature exists
if (typeof expectedValue === 'undefined') {
return true
return false
}

// Handle exact match or comparison
if (typeof expectedValue === 'number') {
return featureValue >= expectedValue // Default to "greater than or equal to" for numbers
}

// Booleans are treated as exact matches
if (typeof featureValue === 'boolean') {
return featureValue
}

if (typeof expectedValue === 'object' && expectedValue.operator && expectedValue.value !== undefined) {
const { operator, value } = expectedValue

Expand Down
8 changes: 4 additions & 4 deletions src/components/ProcessCreate/Questions/useVotingType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import SingleChoice from '~components/ProcessCreate/Questions/SingleChoice'
import { GenericFeatureObject } from '~components/ProcessCreate/Steps/TabsPage'

export const VotingTypeSingle = 'single'
export const UnimplementedVotingTypeApproval = 'approval'
export const VotingTypeApproval = 'approval'

export const MultiQuestionTypes = [VotingTypeSingle]

export type VotingType = typeof VotingTypeSingle | typeof UnimplementedVotingTypeApproval
export type VotingType = typeof VotingTypeSingle | typeof VotingTypeApproval

export const VotingTypes = [VotingTypeSingle as VotingType, UnimplementedVotingTypeApproval as VotingType]
export const VotingTypes = [VotingTypeSingle as VotingType, VotingTypeApproval as VotingType]

export const useVotingType = (): GenericFeatureObject<VotingType> => {
const { t } = useTranslation()
Expand All @@ -24,7 +24,7 @@ export const useVotingType = (): GenericFeatureObject<VotingType> => {
icon: GiChoice,
component: SingleChoice,
},
[UnimplementedVotingTypeApproval]: {
[VotingTypeApproval]: {
title: t('process_create.question.approval_voting.title'),
description: t('process_create.question.approval_voting.description'),
icon: GiChoice,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Box, Checkbox, CheckboxProps, Flex, Icon, Text } from '@chakra-ui/react'
import { Badge, Flex, Icon } from '@chakra-ui/react'
import { useMemo } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { IconType } from 'react-icons'
import { BiCheckDouble } from 'react-icons/bi'
import { useSubscription } from '~components/Auth/Subscription'
import { DetailedBox } from '~components/Layout/Form/DetailedCheckbox'
import { usePricingModal } from '~components/Pricing/Modals'

const useProcessFeatures = () => {
const { t } = useTranslation()
Expand All @@ -13,86 +14,93 @@ const useProcessFeatures = () => {
anonymous: {
title: t('anonymous.title', { defaultValue: 'Anonymous' }),
description: t('anonymous.description', { defaultValue: 'Voters will remain anonymous' }),
boxIcon: BiCheckDouble,
formKey: 'electionType.anonymous',
icon: BiCheckDouble,
name: 'electionType.anonymous',
permission: 'features.anonymous',
},
secretUntilTheEnd: {
title: t('secret_until_the_end.title', { defaultValue: 'Secret until the end' }),
description: t('secret_until_the_end.description', {
defaultValue: 'Vote contents will be encrypted till the end of the voting',
}),
boxIcon: BiCheckDouble,
formKey: 'electionType.secretUntilTheEnd',
icon: BiCheckDouble,
name: 'electionType.secretUntilTheEnd',
},
overwrite: {
title: t('overwrite.title', { defaultValue: 'Vote overwrite' }),
description: t('overwrite.description', { defaultValue: 'Voters will be able to overwrite their vote once' }),
formKey: 'maxVoteOverwrites',
boxIcon: BiCheckDouble,
name: 'maxVoteOverwrites',
icon: BiCheckDouble,
permission: 'features.voteOverwrite',
},
// non implemented features...
// personalization: {
// title: t('personalization.title', { defaultValue: 'personalization' }),
// description: t('personalization.description', { defaultValue: 'personalization' }),
// boxIcon: BiCheckDouble,
// icon: BiCheckDouble,
// },
// emailReminder: {
// title: t('email_reminder.title', { defaultValue: 'Email reminder' }),
// description: t('email_reminder.description', { defaultValue: 'Remind by email' }),
// boxIcon: BiCheckDouble,
// icon: BiCheckDouble,
// },
// smsNotification: {
// title: t('sms_notification.title', { defaultValue: 'SMS Notification' }),
// description: t('sms_notification.description', { defaultValue: 'Notify users somehow (?)' }),
// boxIcon: BiCheckDouble,
// icon: BiCheckDouble,
// },
// whiteLabel: {
// title: t('white_label.title', { defaultValue: 'White label' }),
// description: t('white_label.description', { defaultValue: 'Customize the process layout entirely adding your own logos and color palette' }),
// boxIcon: BiCheckDouble,
// icon: BiCheckDouble,
// },
// liveStreaming: {
// title: t('live_streaming.title', { defaultValue: 'Live Streaming' }),
// description: t('live_streaming.description', { defaultValue: 'IDK what\'s this about' }),
// boxIcon: BiCheckDouble,
// icon: BiCheckDouble,
// },
}),
[t]
)
}

export const Features = () => {
const { subscription } = useSubscription()
const { t } = useTranslation()
const translations = useProcessFeatures()
const { permission } = useSubscription()
const { openModal } = usePricingModal()
const { setValue, getValues } = useFormContext()

return (
<Flex flexDirection='column' gap={6}>
{Object.entries(translations).map(([feature, card], i) => (
<CheckBoxCard key={i} {...card} formKey={card.formKey ?? `saasFeatures.${feature}`} />
))}
</Flex>
)
}
{Object.entries(translations).map(([feature, card], i) => {
const needsUpgrade = 'permission' in card && !permission(card.permission)

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 (
<DetailedBox
key={i}
{...card}
name={card.name ?? `features.${feature}`}
badge={needsUpgrade && <Badge colorScheme='red'>{t('upgrade')}</Badge>}
icon={card.icon && <Icon as={card.icon} />}
isChecked={getValues(card.name)}
onChange={(e) => {
if (!needsUpgrade) {
setValue(card.name, e.target.checked)
return
}

return (
<Checkbox variant='radiobox' {...register(formKey)} {...rest}>
{isPro && <Text as='span'>Pro</Text>}
<Box>
<Icon as={boxIcon} />
<Text>{title}</Text>
</Box>

<Text>{description}</Text>
</Checkbox>
openModal('planUpgrade', {
feature: 'permission' in card && card.permission,
text: translations[feature].title,
})
// reset the value to false
setValue(card.name, false)
return false
}}
/>
)
})}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Flex } from '@chakra-ui/react'
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'
import { FeaturesKeys } from '~components/Pricing/Features'
import { Features as SaasFeaturesComponent } from '~components/ProcessCreate/Settings/SaasFeatures'
import { Features as SaasFeaturesComponent } from '~components/ProcessCreate/Settings/Features'
import { ConfigurationValues } from '~components/ProcessCreate/StepForm/Info'
import { StepsNavigation } from '~components/ProcessCreate/Steps/Navigation'
import Wrapper from '~components/ProcessCreate/Steps/Wrapper'
import { useProcessCreationSteps } from '../Steps/use-steps'

export type SaasFeaturesValues = { saasFeatures: Record<FeaturesKeys, boolean> }
interface FeaturesForm extends SaasFeaturesValues, ConfigurationValues {}
export type FeaturesValues = { features: Record<FeaturesKeys, boolean> }
interface FeaturesForm extends FeaturesValues, ConfigurationValues {}

export const SaasFeatures = () => {
const { form, setForm, next } = useProcessCreationSteps()
Expand Down
1 change: 1 addition & 0 deletions src/components/ProcessCreate/StepForm/Questions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const QuestionsTabs = () => {
<TabsPage
definedList={definedVotingTypes}
unimplementedList={unDefinedVotingTypes}
permissionsPath='votingTypes'
onTabChange={(index: number) => {
const newQuestionType = definedVotingTypes.defined[index]
// If the question type not accepts multiquestion and there are multiple questions selcted store only the first
Expand Down
6 changes: 2 additions & 4 deletions src/components/ProcessCreate/Steps/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ export const StepsForm = ({ steps, activeStep, next, prev, setActiveStep }: Step
weightedVote: false,
questions: [{ options: [{}, {}] }],
addresses: [],
// these do not end up in the election process object, but are required for other purposes
gpsWeighted: false,
passportScore: 20,
stampsUnionType: 'OR',
saasFeatures: {
anonymous: false,
secretUntilTheEnd: true,
overwrite: false,
features: {
personalization: false,
emailReminder: false,
smsNotification: false,
Expand Down
Loading

0 comments on commit 23a2655

Please sign in to comment.