Skip to content

Commit

Permalink
Dashboard, menu and dashboard voting page (#793)
Browse files Browse the repository at this point in the history
* dashboard and dashboard voting page

* Minor dashboard theme fixes

* Rename some elements following their actual routes

* Remade dashboard menu

* Minor style changes

* Fix saas issues in process create theme

* Removed onvote reminiscenses

* Set DashboardContents in the expected views + set some section title
  • Loading branch information
elboletaire authored Oct 28, 2024
1 parent 9f1d5dc commit 9629fe1
Show file tree
Hide file tree
Showing 44 changed files with 979 additions and 606 deletions.
6 changes: 3 additions & 3 deletions src/components/Account/LogoutBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import { Button, ButtonProps } from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '~components/Auth/useAuth'
import useDarkMode from '~components/Layout/useDarkMode'
import { Routes } from '~src/router/routes'

const LogoutBtn = (props?: ButtonProps) => {
const { t } = useTranslation()
const { logout: authLogout } = useAuth()
const navigate = useNavigate()
const { textColorSecondary } = useDarkMode()

const logout = () => {
setTimeout(() => {
Expand All @@ -22,7 +20,9 @@ const LogoutBtn = (props?: ButtonProps) => {
<Button
variant='outline'
border='none'
color={textColorSecondary}
bg='transparent'
color='contents.color.light'
_dark={{ color: 'contents.color.dark' }}
textDecoration='underline'
_hover={{ textDecoration: 'none' }}
{...props}
Expand Down
35 changes: 35 additions & 0 deletions src/components/Dashboard/Menu/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
import { Button, Icon } from '@chakra-ui/react'
import { Link as ReactRouterLink, generatePath } from 'react-router-dom'

export const DashboardMenuItem = ({
label,
route,
icon,
isOpen = false,
isActive = false,
onToggle,
hasChildren = false,
}: {
label: string
route?: string
icon?: any
isOpen?: boolean
isActive?: boolean
onToggle?: () => void
hasChildren?: boolean
}) => (
<Button
as={route ? ReactRouterLink : undefined}
to={route ? generatePath(route) : undefined}
onClick={hasChildren ? onToggle : undefined}
isActive={isActive} // Set active state
justifyContent='start'
variant='menu'
w='full'
leftIcon={icon ? <Icon as={icon} /> : undefined}
rightIcon={hasChildren ? isOpen ? <ChevronUpIcon /> : <ChevronDownIcon /> : undefined}
>
{label}
</Button>
)
130 changes: 130 additions & 0 deletions src/components/Dashboard/Menu/Options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Box, Collapse } from '@chakra-ui/react'
import { OrganizationName } from '@vocdoni/chakra-components'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FaPhoneAlt } from 'react-icons/fa'
import { FaHouse } from 'react-icons/fa6'
import { GiHamburgerMenu } from 'react-icons/gi'
import { HiMiniPencil, HiSquares2X2 } from 'react-icons/hi2'
import { IoIosSettings } from 'react-icons/io'
import { matchPath, useLocation } from 'react-router-dom'
import { Routes } from '~src/router/routes'
import { DashboardMenuItem } from './Item'

type MenuItem = {
label: string
icon?: any
route?: string
children?: MenuItem[]
}

export const DashboardMenuOptions = () => {
const { t } = useTranslation()
const location = useLocation()
const [openSection, setOpenSection] = useState<string | null>(null)

const menuItems: MenuItem[] = [
{
label: t('organization.dashboard'),
icon: FaHouse,
route: Routes.dashboard.base,
},
{
label: t('voting_processes'),
icon: HiSquares2X2,
children: [
{ label: t('all'), route: Routes.dashboard.processes },
{ label: t('active'), route: '#active' },
{ label: t('finished'), route: '#finished' },
{ label: t('draft'), route: '#draft' },
],
},
{
label: t('organization.census'),
icon: GiHamburgerMenu,
route: '#census',
},
{
label: t('user_management'),
icon: HiMiniPencil,
route: '#user-management',
},
{
label: t('settings'),
icon: IoIosSettings,
children: [
{ label: t('organization.organization'), route: '#organization' },
{ label: t('team'), route: Routes.dashboard.team },
{ label: t('billing'), route: '#billing' },
{ label: t('subscription'), route: '#subscription' },
{ label: t('profile'), route: Routes.dashboard.profile },
],
},
{
label: t('help_and_support'),
icon: FaPhoneAlt,
route: '#support',
},
]

// Check if any child route is active, and open its section
useEffect(() => {
menuItems.forEach((item) => {
if (
item.children &&
item.children.some((child) => matchPath({ path: child.route || '', end: true }, location.pathname))
) {
setOpenSection((prev) => (prev !== item.label ? item.label : prev))
}
})
}, [location.pathname])

const handleToggle = (label: string) => {
setOpenSection((prev) => (prev === label ? null : label))
}

return (
<Box>
<OrganizationName color='text.secondary' mb={2.5} />
{menuItems.map((item, index) => (
<Box key={index}>
{item.children ? (
<>
<DashboardMenuItem
label={item.label}
icon={item.icon}
route={item.route}
isOpen={openSection === item.label}
isActive={item.children.some((child) =>
matchPath({ path: child.route || '', end: true }, location.pathname)
)}
onToggle={() => handleToggle(item.label)}
hasChildren
/>
<Collapse in={openSection === item.label}>
<Box pl={6}>
{item.children.map((child, childIndex) => (
<DashboardMenuItem
key={childIndex}
label={child.label}
route={child.route}
icon={null}
isActive={Boolean(matchPath({ path: child.route || '', end: true }, location.pathname))}
/>
))}
</Box>
</Collapse>
</>
) : (
<DashboardMenuItem
label={item.label}
route={item.route}
icon={item.icon}
isActive={Boolean(matchPath({ path: item.route || '', end: true }, location.pathname))}
/>
)}
</Box>
))}
</Box>
)
}
60 changes: 60 additions & 0 deletions src/components/Dashboard/Menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { AddIcon } from '@chakra-ui/icons'
import { Box, Button, Drawer, DrawerContent, DrawerOverlay } from '@chakra-ui/react'
import { Trans } from 'react-i18next'
import { Link as ReactRouterLink, generatePath } from 'react-router-dom'
import LogoutBtn from '~components/Account/LogoutBtn'
import { HSeparator } from '~components/Auth/SignIn'
import { ColorModeSwitcher } from '~components/Layout/ColorModeSwitcher'
import { VocdoniLogo } from '~components/Layout/Logo'
import { Routes } from '~src/router/routes'
import { DashboardMenuOptions } from './Options'

const DashboardMenu = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => (
<>
{/* Sidebar for large screens */}
<Box
gridArea='sidebar'
p={4}
display={{ base: 'none', lg: 'flex' }}
flexDirection='column'
gap={4}
bg='dashboard.sidebar.bg.light'
_dark={{ bg: 'dashboard.sidebar.bg.dark' }}
>
<DashboardMenuContent />
</Box>

{/* Sidebar for small screens */}
<Drawer isOpen={isOpen} placement='left' onClose={onClose}>
<DrawerOverlay />
<DrawerContent bg='dashboard.sidebar.bg.light' _dark={{ bg: 'dashboard.sidebar.bg.dark' }}>
<Box p={4} display='flex' flexDirection='column' gap={4}>
<DashboardMenuContent />
</Box>
</DrawerContent>
</Drawer>
</>
)

// Common menu contents
const DashboardMenuContent = () => (
<>
<VocdoniLogo w='180px' alignSelf='center' />
<HSeparator />
<DashboardMenuOptions />
<Button
as={ReactRouterLink}
to={generatePath(Routes.processes.create)}
variant='box-shadow'
w='full'
my={5}
leftIcon={<AddIcon />}
>
<Trans i18nKey='new_voting'>New voting</Trans>
</Button>
<LogoutBtn />
<ColorModeSwitcher ml='auto' />
</>
)

export default DashboardMenu
126 changes: 126 additions & 0 deletions src/components/Dashboard/PricingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
Box,
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Select,
Text,
} from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'
import { Link as ReactRouterLink } from 'react-router-dom'
import PricingCard from '~components/Organization/Dashboard/PricingCard'

type CardProps = {
popular: boolean
title: string
subtitle: string
price: string
features: string[]
}

export const PricingModal = ({ isOpenModal, onCloseModal }: { isOpenModal: boolean; onCloseModal: () => void }) => {
const { t } = useTranslation()

const cards: CardProps[] = [
{
popular: false,
title: t('pricing_modal.essential_title', { defaultValue: 'Essential' }),
subtitle: t('pricing_modal.essential_subtitle', {
defaultValue: 'Small or medium-sized orgs or community groups with basic voting needs.',
}),
price: '99',
features: [
t('pricing_modal.essential_feat.core_voting', { defaultValue: 'Core voting features' }),
t('pricing_modal.essential_feat.up_to_admins', { defaultValue: 'Up to 3 Admins and 1 org' }),
t('pricing_modal.essential_feat.yearly_processes', { defaultValue: '5 yearly voting processes' }),
t('pricing_modal.essential_feat.report_analytitcs', { defaultValue: 'Basic reporting and analytics' }),
t('pricing_modal.essential_feat.ticket_support', { defaultValue: 'Ticket support' }),
t('pricing_modal.essential_feat.gpdr_compilance', { defaultValue: 'GDPR compliance' }),
],
},
{
popular: true,
title: t('pricing_modal.premium_title', { defaultValue: 'Premium' }),
subtitle: t('pricing_modal.premium_subtitle', {
defaultValue: 'Larger amount that require more advanced features.',
}),
price: '389',
features: [
t('pricing_modal.premium_feat.all_essential', { defaultValue: 'All essential plan features' }),
t('pricing_modal.premium_feat.up_to_admins', { defaultValue: 'Up to 5 Admins' }),
t('pricing_modal.premium_feat.yearly_processes', { defaultValue: '10 yearly voting processes' }),
t('pricing_modal.premium_feat.custom_subdomain', { defaultValue: 'Custom subdomain & branding' }),
t('pricing_modal.premium_feat.report_analytitcs', { defaultValue: 'Advanced reporting & analytics' }),
t('pricing_modal.premium_feat.ticket_support', { defaultValue: 'Ticket and chat support' }),
t('pricing_modal.premium_feat.gpdr_compilance', { defaultValue: 'GDPR compliance' }),
],
},
{
popular: false,
title: t('pricing_modal.custom_title', { defaultValue: 'Custom Plan' }),
subtitle: t('pricing_modal.custom_subtitle', {
defaultValue:
'Large organizations enterprises, and institutions requiring extensive customization and support.',
}),
price: '999',
features: [
t('pricing_modal.custom_feat.all_votings', { defaultValue: 'All faetures & voting types' }),
t('pricing_modal.custom_feat.unlimited_admins', { defaultValue: 'Unlimited Admins & suborgs' }),
t('pricing_modal.custom_feat.unlimited_processes', { defaultValue: 'Unlimited yearly voting processes' }),
t('pricing_modal.custom_feat.white_label_solution', { defaultValue: 'White-label solution' }),
t('pricing_modal.custom_feat.advanced_security', { defaultValue: 'Advanced security features' }),
t('pricing_modal.custom_feat.account_manager', { defaultValue: 'Dedicated account manager' }),
t('pricing_modal.custom_feat.full_tech_support', {
defaultValue: 'Full technical support (ticket, chat, email)',
}),
],
},
]
return (
<Modal isOpen={isOpenModal} onClose={onCloseModal} variant='dashboard-plans'>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Trans i18nKey='pricing_modal.title'>You need to upgrade to use this feature</Trans>
</ModalHeader>
<ModalCloseButton fontSize='lg' />
<ModalBody>
{cards.map((card, idx) => (
<PricingCard key={idx} {...card} />
))}
</ModalBody>

<ModalFooter>
<Box>
<Text>
<Trans i18nKey='pricing_modal.more_voters'>If you need more voters, you can select it here:</Trans>
</Text>
<Select>
<option>1-500 members</option>
</Select>
</Box>
<Text>
<Trans i18nKey='pricing_modal.your_plan'>
Currently you are subscribed to the 'Your plan' subscription. If you upgrade, we will only charge the
yearly difference. In the next billing period, starting on 'dd/mm/yy' you will pay for the new select
plan.
</Trans>
</Text>
<Box>
<Text>
<Trans i18nKey='pricing_modal.help'>Need some help?</Trans>
</Text>
<Button as={ReactRouterLink} colorScheme='white'>
<Trans i18nKey='contact_us'>Contact us</Trans>
</Button>
</Box>
</ModalFooter>
</ModalContent>
</Modal>
)
}
Loading

2 comments on commit 9629fe1

@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.