Skip to content

Commit f709857

Browse files
gerouvielboletaire
andauthored
F/UI fixes review (#975)
* feat: add max duration validation to calendar component (#972) * feat: add max duration validation to calendar component - Add subscription max duration validation - Show warning alert when duration exceeds plan limit * feat: add form validation for max duration - Add validation rule to endDate field - Prevent form submission when duration exceeds plan limit - Improve UX with error styling and upgrade plan button - Only show info card when duration is valid * fix: create org state issues (#965) fix: create org state issues * review --------- Co-authored-by: Òscar Casajuana <[email protected]>
1 parent 60d0fe0 commit f709857

21 files changed

+225
-157
lines changed

src/components/Account/Edit.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Teams from './Teams'
99
export const AccountEdit = () => {
1010
const { data: profile } = useProfile()
1111
return (
12-
<Flex flexDirection='column' h='97%' mt='20px'>
12+
<Flex flexDirection='column'>
1313
<Tabs w='full' maxW={InnerContentsMaxWidth} mx='auto' isFitted>
1414
<TabList>
1515
<Tab>

src/components/Auth/useAuthProvider.ts

-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ export const useAuthProvider = () => {
113113
localStorage.removeItem(LocalStorageKeys.Token)
114114
localStorage.removeItem(LocalStorageKeys.Expiry)
115115
localStorage.removeItem(LocalStorageKeys.RenewSession)
116-
localStorage.removeItem(LocalStorageKeys.SignerAddress)
117116
setBearer(null)
118117
clear()
119118
}, [])

src/components/Dashboard/Menu/Item.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const DashboardMenuItem = ({
3636
rightIcon={hasChildren ? isOpen ? <ChevronUpIcon /> : <ChevronDownIcon /> : undefined}
3737
mb={hasChildren && 1}
3838
fontWeight={isActive ? '600' : '100'}
39-
fontSize='16px'
39+
fontSize='md'
4040
fontFamily='basier'
4141
{...props}
4242
>

src/components/Dashboard/Menu/OrganizationSwitcher.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const OrganizationSwitcher = () => {
3636
for (const org of profile.organizations) {
3737
const address = org.organization.address
3838
try {
39-
const data = await client.fetchAccount(address)
39+
const data = await client.fetchAccountInfo(address)
4040
names[address] = data?.account?.name?.default || address
4141
} catch (error) {
4242
console.error('Error fetching organization name:', error)
@@ -68,6 +68,16 @@ export const OrganizationSwitcher = () => {
6868
}
6969
}, [organizations, selectedOrg])
7070

71+
// Sync selected organization with localStorage when profile changes
72+
useEffect(() => {
73+
if (!profile?.organizations) return
74+
75+
const storedAddress = localStorage.getItem(LocalStorageKeys.SignerAddress)
76+
if (storedAddress !== selectedOrg) {
77+
setSelectedOrg(storedAddress)
78+
}
79+
}, [profile])
80+
7181
const handleOrgChange = async (option: SelectOption | null) => {
7282
if (!option) return
7383
setSelectedOrg(option.value)

src/components/Dashboard/Menu/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const DashboardMenu = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => vo
4444
const DashboardMenuContent = () => (
4545
<>
4646
<Link as={RouterLink} to={Routes.dashboard.base} alignSelf='center'>
47-
<VocdoniLogo w='130px' m='5px auto' />
47+
<VocdoniLogo w='130px' mx='5px' />
4848
</Link>
4949
<HSeparator mb='10px' />
5050
<DashboardMenuOptions />

src/components/Layout/Dashboard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ export const DashboardBox = (props: BoxProps) => (
1212
)
1313

1414
export const DashboardContents = (props: BoxProps) => (
15-
<DashboardBox borderRadius='lg' height='100%' py={{ base: 4, lg: 6 }} px={{ base: 4, lg: 10 }} {...props} />
15+
<DashboardBox borderRadius='lg' height='100%' py={{ base: 4, lg: 10 }} px={{ base: 4, lg: 10 }} {...props} />
1616
)
+15-17
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
import { Box, Button, Card, CardBody, Flex, Img, Text } from '@chakra-ui/react'
1+
import { Box, Card, CardBody, Flex, Img, Text } from '@chakra-ui/react'
22
import { Trans } from 'react-i18next'
33
import empty from '/assets/illustrations/2.png'
44

5-
export const NoResultsFiltering = () => {
6-
return (
7-
<Card variant='no-elections' minH='100%' maxW='650' m='80px auto'>
8-
<CardBody>
9-
<Flex justifyContent={'center'}>
10-
<Img src={empty} />
11-
</Flex>
12-
<Box mt='50px' textAlign='center'>
13-
<Text as='h3' fontSize='md' fontWeight='bold'>
14-
<Trans i18nKey='no_results_filtering'>Your current search filter returns no results</Trans>
15-
</Text>
16-
</Box>
17-
</CardBody>
18-
</Card>
19-
)
20-
}
5+
export const NoResultsFiltering = () => (
6+
<Card variant='no-elections' minH='100%' maxW='650' m='80px auto'>
7+
<CardBody>
8+
<Flex justifyContent={'center'}>
9+
<Img src={empty} />
10+
</Flex>
11+
<Box mt='50px' textAlign='center'>
12+
<Text as='h3' fontSize='md' fontWeight='bold'>
13+
<Trans i18nKey='no_results_filtering'>Your current search filter returns no results</Trans>
14+
</Text>
15+
</Box>
16+
</CardBody>
17+
</Card>
18+
)

src/components/Navbar/LanguagesList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const LanguagesListAccordion = () => {
106106
m={0}
107107
h={'fit-content'}
108108
w={'fit-content'}
109-
mt='5px'
109+
my='5px'
110110
>
111111
{k.toUpperCase()}
112112
</Button>

src/components/Organization/Create.tsx

+71-68
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button, Flex, FlexProps, Text } from '@chakra-ui/react'
33
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
44
import { useClient } from '@vocdoni/react-providers'
55
import { Account, RemoteSigner } from '@vocdoni/sdk'
6-
import { useEffect, useState } from 'react'
6+
import { useState } from 'react'
77
import { FormProvider, useForm } from 'react-hook-form'
88
import { Trans, useTranslation } from 'react-i18next'
99
import { Link as ReactRouterLink, To, useNavigate } from 'react-router-dom'
@@ -21,17 +21,60 @@ type FormData = PrivateOrgFormData & CreateOrgParams
2121

2222
type OrganizationCreateResponse = {
2323
address: string
24+
account: Account
25+
signer: RemoteSigner
26+
client: ReturnType<typeof useClient>['client']
2427
}
2528

2629
const useOrganizationCreate = (
27-
options?: Omit<UseMutationOptions<OrganizationCreateResponse, Error, CreateOrgParams>, 'mutationFn'>
30+
options?: Omit<UseMutationOptions<OrganizationCreateResponse, Error, FormData>, 'mutationFn'>
2831
) => {
2932
const { bearedFetch } = useAuth()
30-
const client = useQueryClient()
31-
return useMutation<OrganizationCreateResponse, Error, CreateOrgParams>({
32-
mutationFn: (params: CreateOrgParams) => bearedFetch(ApiEndpoints.Organizations, { body: params, method: 'POST' }),
33-
onSuccess: () => {
34-
client.invalidateQueries({ queryKey: QueryKeys.profile })
33+
const { client, setSigner, signer: csigner } = useClient()
34+
const { bearer, signerRefresh } = useAuthProvider()
35+
const qclient = useQueryClient()
36+
37+
return useMutation<OrganizationCreateResponse, Error, FormData>({
38+
mutationFn: async (values: FormData) => {
39+
// Create account on the saas to generate new priv keys
40+
const { address }: { address: string } = await bearedFetch(ApiEndpoints.Organizations, {
41+
body: {
42+
name: values.name,
43+
website: values.website,
44+
description: values.description,
45+
size: values.size?.value,
46+
country: values.country?.value,
47+
type: values.type?.value,
48+
communications: values.communications,
49+
},
50+
method: 'POST',
51+
})
52+
53+
const signer = new RemoteSigner({
54+
url: import.meta.env.SAAS_URL,
55+
token: bearer,
56+
})
57+
58+
signer.address = address
59+
client.wallet = signer
60+
61+
const account = new Account({
62+
name: typeof values.name === 'object' ? values.name.default : values.name,
63+
description: typeof values.description === 'object' ? values.description.default : values.description,
64+
})
65+
66+
await client.createAccount({ account })
67+
68+
localStorage.setItem(LocalStorageKeys.SignerAddress, address)
69+
qclient.invalidateQueries({ queryKey: QueryKeys.profile })
70+
71+
// Refresh the signer if it was already set
72+
if (csigner !== null) {
73+
setSigner(signer)
74+
await signerRefresh()
75+
}
76+
77+
return { address, account, signer, client }
3578
},
3679
...options,
3780
})
@@ -49,73 +92,33 @@ export const OrganizationCreate = ({
4992
const navigate = useNavigate()
5093
const [isPending, setIsPending] = useState(false)
5194
const methods = useForm<FormData>()
95+
const { fetchAccount } = useClient()
5296
const { handleSubmit } = methods
53-
const { bearer, signerRefresh } = useAuthProvider()
54-
const { client, fetchAccount, setClient, setSigner, signer: osigner } = useClient()
55-
const [promiseError, setPromiseError] = useState<Error | null>(null)
56-
const [redirect, setRedirect] = useState<To | null>(null)
57-
58-
const { mutateAsync: createSaasAccount, isError: isSaasError, error: saasError } = useOrganizationCreate()
5997

60-
const error = saasError || promiseError
61-
const isError = isSaasError || !!promiseError
98+
const {
99+
mutateAsync: createOrganization,
100+
isError,
101+
error,
102+
} = useOrganizationCreate({
103+
onSuccess: async ({ signer }) => {
104+
navigate(onSuccessRoute)
105+
setTimeout(async () => {
106+
await fetchAccount()
107+
}, 50)
108+
},
109+
})
62110

63-
const onSubmit = (values: FormData) => {
111+
const onSubmit = async (values: FormData) => {
64112
setIsPending(true)
65-
// Create account on the saas to generate new priv keys
66-
createSaasAccount({
67-
name: values.name,
68-
website: values.website,
69-
description: values.description,
70-
size: values.size?.value,
71-
country: values.country?.value,
72-
type: values.type?.value,
73-
communications: values.communications,
74-
})
75-
.then(({ address }: { address: string }) => {
76-
const signer = new RemoteSigner({
77-
url: import.meta.env.SAAS_URL,
78-
token: bearer,
79-
})
80-
81-
signer.address = address
82-
client.wallet = signer
83-
84-
// setting the signer when there's no signer causes the page to reload
85-
if (osigner !== null) {
86-
setSigner(signer)
87-
setClient(client)
88-
}
89-
localStorage.setItem(LocalStorageKeys.SignerAddress, address)
90-
91-
return client.createAccount({
92-
account: new Account({
93-
name: typeof values.name === 'object' ? values.name.default : values.name,
94-
description: typeof values.description === 'object' ? values.description.default : values.description,
95-
}),
96-
})
97-
})
98-
// store redirect
99-
.then(() => {
100-
setRedirect(onSuccessRoute)
101-
})
102-
.catch((e) => {
103-
setPromiseError(e)
104-
})
105-
.finally(() => {
106-
setIsPending(false)
107-
})
113+
try {
114+
await createOrganization(values)
115+
} catch (e) {
116+
// Error handling is managed by react-query
117+
} finally {
118+
setIsPending(false)
119+
}
108120
}
109121

110-
// redirect on success
111-
useEffect(() => {
112-
if (!redirect) return
113-
// update account and signer
114-
fetchAccount().then(() => signerRefresh())
115-
// "redirect"
116-
navigate(redirect)
117-
}, [redirect])
118-
119122
return (
120123
<FormProvider {...methods}>
121124
<Flex

src/components/Organization/Edit.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ const EditOrganization = () => {
138138
gap={6}
139139
maxW={InnerContentsMaxWidth}
140140
mx='auto'
141-
my='30px'
141+
my='10px'
142142
onSubmit={handleSubmit(onSubmit)}
143143
>
144144
<PublicOrgForm />

src/components/Organization/NoOrganizations.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ export const NoOrganizations = () => {
1717
<Trans i18nKey='organization.no_organization_title'>You don't belong to any organization yet!</Trans>
1818
</Text>
1919
<Text mb='20px'>
20-
<Trans
21-
i18nKey='new_organization.description1'
22-
components={{ span: <Text as='span' fontWeight='600' color='#000' /> }}
23-
>
20+
<Trans i18nKey='new_organization.description1' components={{ span: <Text as='span' fontWeight='600' /> }}>
2421
Set up your{' '}
2522
<Text as='span' fontWeight='bold'>
2623
organization for free

src/components/Organization/Subscription.tsx

+13-29
Original file line numberDiff line numberDiff line change
@@ -107,68 +107,52 @@ export const SubscriptionList = () => {
107107
</Alert>
108108
)}
109109
<TableContainer w='full'>
110-
<Table size='sm'>
110+
<Table size='sm' variant={'subscription'}>
111111
<Thead>
112112
<Tr>
113113
<Th>
114114
<Trans i18nKey='subscription.your_subscription'>Your Subscription</Trans>
115115
</Th>
116-
<Th textAlign='center'>
116+
<Th>
117117
<Trans i18nKey='subscription.price'>Price</Trans>
118118
</Th>
119-
<Th textAlign='center'>
119+
<Th>
120120
<Trans i18nKey='subscription.since'>Since</Trans>
121121
</Th>
122-
<Th textAlign='center'>
122+
<Th>
123123
<Trans i18nKey='subscription.next_billing'>Next Billing</Trans>
124124
</Th>
125-
<Th textAlign='center'>Action</Th>
125+
<Th>
126+
<Trans i18nKey='subscription.action'>Action</Trans>
127+
</Th>
126128
</Tr>
127129
</Thead>
128130
<Tbody>
129-
<Tr h='75px'>
131+
<Tr>
130132
<Td>
131133
<Box display='flex' alignItems='center' gap={3}>
132134
<Avatar name={subscription.plan.name} size='sm' />
133135
{subscription.plan.name} ({subscription.plan.organization.maxCensus} members)
134136
</Box>
135137
</Td>
136138
<Td>
137-
<Tag style={{ border: '0px', padding: '6px 15px', color: '#193d32', margin: '0px auto' }}>
138-
{currency(subscription.plan.startingPrice)}
139-
</Tag>
139+
<Tag>{currency(subscription.plan.startingPrice)}</Tag>
140140
</Td>
141141
<Td>
142-
<Tag style={{ border: '0px', padding: '6px 15px', color: '#193d32', margin: '0px auto' }}>
143-
{new Date(subscription.subscriptionDetails.startDate).toLocaleDateString()}
144-
</Tag>
142+
<Tag>{new Date(subscription.subscriptionDetails.startDate).toLocaleDateString()}</Tag>
145143
</Td>
146144
<Td>
147-
<Tag style={{ border: '0px', padding: '6px 15px', color: '#193d32', margin: '0px auto' }}>
148-
{new Date(subscription.subscriptionDetails.renewalDate).toLocaleDateString()}
149-
</Tag>
145+
<Tag>{new Date(subscription.subscriptionDetails.renewalDate).toLocaleDateString()}</Tag>
150146
</Td>
151147
{isFree ? (
152148
<Td>
153-
<Button
154-
style={{ margin: '0px auto' }}
155-
variant='primary'
156-
size='sm'
157-
isLoading={isLoading}
158-
onClick={() => openModal('subscription')}
159-
>
149+
<Button variant='primary' size='sm' isLoading={isLoading} onClick={() => openModal('subscription')}>
160150
<Trans i18nKey='upgrade'>Upgrade</Trans>
161151
</Button>
162152
</Td>
163153
) : (
164154
<Td>
165-
<Button
166-
style={{ margin: '0px auto' }}
167-
variant='outline'
168-
size='sm'
169-
isLoading={isLoading}
170-
onClick={() => handleChangeClick()}
171-
>
155+
<Button variant='outline' size='sm' isLoading={isLoading} onClick={() => handleChangeClick()}>
172156
<Trans i18nKey='subscription.change_plan_button'>Change</Trans>
173157
</Button>
174158
</Td>

src/components/Organization/Team.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ const TeamMembersTable = ({ members, showExpiration = false }: { members: Member
110110
</Td>
111111
)}
112112
<Td textAlign='center' w={10}>
113-
<Badge p='8px 20px' textTransform='capitalize' border='1px solid #999'>
114-
{member.role}
115-
</Badge>
113+
<Badge>{member.role}</Badge>
116114
</Td>
117115
</Tr>
118116
))}

0 commit comments

Comments
 (0)