-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Organization edit should be organization (alone) * Rename organization edit, to properly reflect its scope * No need for a folder here (yet) * account profile pages * Org<->Account renamings * Type fixes
- Loading branch information
1 parent
19e89f4
commit cb58c01
Showing
24 changed files
with
507 additions
and
51 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Button, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react' | ||
import { Trans } from 'react-i18next' | ||
import { InnerContentsMaxWidth } from '~constants' | ||
import { useProfile } from '~src/queries/account' | ||
import AccountForm from './Form' | ||
import PasswordForm from './PasswordForm' | ||
import Teams from './Teams' | ||
|
||
export const AccountEdit = () => { | ||
const { data: profile } = useProfile() | ||
return ( | ||
<Flex flexDirection='column' h='100%'> | ||
<Tabs w='full' maxW={InnerContentsMaxWidth} mx='auto'> | ||
<TabList> | ||
<Tab> | ||
<Trans i18nKey='profile'>Profile</Trans> | ||
</Tab> | ||
<Tab> | ||
<Trans i18nKey='password'>Password</Trans> | ||
</Tab> | ||
<Tab> | ||
<Trans i18nKey='teams'>Teams</Trans> | ||
</Tab> | ||
</TabList> | ||
|
||
<TabPanels> | ||
<TabPanel> | ||
<AccountForm profile={profile} /> | ||
</TabPanel> | ||
<TabPanel> | ||
<PasswordForm /> | ||
</TabPanel> | ||
<TabPanel> | ||
<Teams roles={profile.organizations} /> | ||
</TabPanel> | ||
</TabPanels> | ||
</Tabs> | ||
<Button colorScheme='red' variant='outline' alignSelf='center' mt='auto' isDisabled> | ||
<Trans i18nKey='delete_my_account'>Delete my account</Trans> | ||
</Button> | ||
</Flex> | ||
) | ||
} |
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 { | ||
Avatar, | ||
Button, | ||
Flex, | ||
FormControl, | ||
FormErrorMessage, | ||
FormLabel, | ||
Input, | ||
Text, | ||
useToast, | ||
VStack, | ||
} from '@chakra-ui/react' | ||
import { useForm } from 'react-hook-form' | ||
import { useTranslation } from 'react-i18next' | ||
import { User, useUpdateProfile } from '~src/queries/account' | ||
|
||
interface ProfileFormData { | ||
firstName: string | ||
lastName: string | ||
email: string | ||
} | ||
|
||
const AccountForm = ({ profile }: { profile: User }) => { | ||
const { t } = useTranslation() | ||
const toast = useToast() | ||
const updateProfile = useUpdateProfile() | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors, isSubmitting }, | ||
} = useForm<ProfileFormData>({ | ||
values: profile | ||
? { | ||
firstName: profile.firstName, | ||
lastName: profile.lastName, | ||
email: profile.email, | ||
} | ||
: undefined, | ||
}) | ||
|
||
const onSubmit = async (data: ProfileFormData) => { | ||
try { | ||
await updateProfile.mutateAsync({ | ||
firstName: data.firstName, | ||
lastName: data.lastName, | ||
}) | ||
|
||
toast({ | ||
title: t('profile.success', { defaultValue: 'Profile updated successfully' }), | ||
status: 'success', | ||
}) | ||
} catch (error) { | ||
toast({ | ||
title: t('profile.error', { defaultValue: 'Failed to update profile' }), | ||
status: 'error', | ||
}) | ||
} | ||
} | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<VStack spacing={8} align='stretch'> | ||
<FormControl isDisabled style={{ cursor: 'not-allowed' }}> | ||
<FormLabel>{t('profile.avatar.label', { defaultValue: 'Avatar' })}</FormLabel> | ||
<Flex align='center' gap={4}> | ||
<Avatar size='lg' name={profile ? `${profile.firstName} ${profile.lastName}` : undefined} /> | ||
<Text color='gray.500' fontSize='sm'> | ||
{t('avatar.hint', { defaultValue: 'Min 200x200px .PNG or .JPEG' })} | ||
</Text> | ||
</Flex> | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.firstName}> | ||
<FormLabel>{t('name.label', { defaultValue: 'Name' })}</FormLabel> | ||
<Input | ||
{...register('firstName', { | ||
required: t('name.required', { defaultValue: 'Name is required' }), | ||
})} | ||
/> | ||
<FormErrorMessage>{errors.firstName?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.lastName}> | ||
<FormLabel>{t('surname.label', { defaultValue: 'Surname' })}</FormLabel> | ||
<Input | ||
{...register('lastName', { | ||
required: t('surname.required', { defaultValue: 'Surname is required' }), | ||
})} | ||
/> | ||
<FormErrorMessage>{errors.lastName?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.email}> | ||
<FormLabel>{t('email.label', { defaultValue: 'Email' })}</FormLabel> | ||
<Input {...register('email')} isDisabled type='email' /> | ||
<FormErrorMessage>{errors.email?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<Button type='submit' size='lg' isLoading={isSubmitting || updateProfile.isPending}> | ||
{t('actions.save', { defaultValue: 'Save Changes' })} | ||
</Button> | ||
</VStack> | ||
</form> | ||
) | ||
} | ||
|
||
export default AccountForm |
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,49 @@ | ||
import { Avatar, Box, BoxProps, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner } from '@chakra-ui/react' | ||
import { Trans } from 'react-i18next' | ||
import { Link as RouterLink } from 'react-router-dom' | ||
import { useAuth } from '~components/Auth/useAuth' | ||
import { useProfile } from '~src/queries/account' | ||
import { Routes } from '~src/router/routes' | ||
|
||
const AccountMenu: React.FC<BoxProps> = (props) => { | ||
const { logout } = useAuth() | ||
const { data: profile, isLoading } = useProfile() | ||
|
||
if (isLoading) { | ||
return ( | ||
<Box {...props}> | ||
<Spinner /> | ||
</Box> | ||
) | ||
} | ||
|
||
return ( | ||
<Box {...props}> | ||
<Menu> | ||
<MenuButton | ||
as={IconButton} | ||
icon={ | ||
<Avatar | ||
name={`${profile.firstName} ${profile.lastName}`} | ||
src={profile.organizations[0]?.organization.logo || ''} | ||
size='sm' | ||
/> | ||
} | ||
size='sm' | ||
variant='outline' | ||
aria-label='User menu' | ||
/> | ||
<MenuList> | ||
<MenuItem as={RouterLink} to={Routes.dashboard.profile}> | ||
<Trans i18nKey='profile'>Profile</Trans> | ||
</MenuItem> | ||
<MenuItem onClick={logout}> | ||
<Trans i18nKey='logout'>Logout</Trans> | ||
</MenuItem> | ||
</MenuList> | ||
</Menu> | ||
</Box> | ||
) | ||
} | ||
|
||
export default AccountMenu |
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,116 @@ | ||
import { Button, FormControl, FormErrorMessage, FormLabel, Input, useToast, VStack } from '@chakra-ui/react' | ||
import { useMutation } from '@tanstack/react-query' | ||
import { useForm } from 'react-hook-form' | ||
import { useTranslation } from 'react-i18next' | ||
import { ApiEndpoints } from '~components/Auth/api' | ||
import { useAuth } from '~components/Auth/useAuth' | ||
|
||
interface PasswordFormData { | ||
oldPassword: string | ||
newPassword: string | ||
confirmPassword: string | ||
} | ||
|
||
export interface UpdatePasswordParams { | ||
oldPassword: string | ||
newPassword: string | ||
} | ||
|
||
const useUpdatePassword = () => { | ||
const { bearedFetch } = useAuth() | ||
|
||
return useMutation<void, Error, UpdatePasswordParams>({ | ||
mutationFn: (params) => | ||
bearedFetch<void>(ApiEndpoints.Password, { | ||
method: 'PUT', | ||
body: params, | ||
}), | ||
}) | ||
} | ||
|
||
const PasswordForm = () => { | ||
const { t } = useTranslation() | ||
const toast = useToast() | ||
const updatePassword = useUpdatePassword() | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
watch, | ||
reset, | ||
formState: { errors, isSubmitting }, | ||
} = useForm<PasswordFormData>() | ||
|
||
const password = watch('newPassword') | ||
|
||
const onSubmit = async (data: PasswordFormData) => { | ||
try { | ||
await updatePassword.mutateAsync({ | ||
oldPassword: data.oldPassword, | ||
newPassword: data.newPassword, | ||
}) | ||
|
||
toast({ | ||
title: t('password.success', { defaultValue: 'Password updated successfully' }), | ||
status: 'success', | ||
}) | ||
reset() | ||
} catch (error) { | ||
toast({ | ||
title: t('password.error', { defaultValue: 'Failed to update password' }), | ||
status: 'error', | ||
}) | ||
} | ||
} | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<VStack spacing={6} align='stretch'> | ||
<FormControl isInvalid={!!errors.oldPassword}> | ||
<FormLabel>{t('password.old.label', { defaultValue: 'Current Password' })}</FormLabel> | ||
<Input | ||
type='password' | ||
{...register('oldPassword', { | ||
required: t('password.old.required', { defaultValue: 'Current password is required' }), | ||
})} | ||
/> | ||
<FormErrorMessage>{errors.oldPassword?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.newPassword}> | ||
<FormLabel>{t('password.new.label', { defaultValue: 'New Password' })}</FormLabel> | ||
<Input | ||
type='password' | ||
{...register('newPassword', { | ||
required: t('password.new.required', { defaultValue: 'Password is required' }), | ||
minLength: { | ||
value: 8, | ||
message: t('password.new.minLength', { defaultValue: 'Password must be at least 8 characters' }), | ||
}, | ||
})} | ||
/> | ||
<FormErrorMessage>{errors.newPassword?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.confirmPassword}> | ||
<FormLabel>{t('password.confirm.label', { defaultValue: 'Confirm Password' })}</FormLabel> | ||
<Input | ||
type='password' | ||
{...register('confirmPassword', { | ||
required: t('password.confirm.required', { defaultValue: 'Please confirm your password' }), | ||
validate: (value) => | ||
value === password || t('password.confirm.mismatch', { defaultValue: "Passwords don't match" }), | ||
})} | ||
/> | ||
<FormErrorMessage>{errors.confirmPassword?.message}</FormErrorMessage> | ||
</FormControl> | ||
|
||
<Button type='submit' size='lg' width='100%' isLoading={isSubmitting || updatePassword.isPending} mt={4}> | ||
{t('password.actions.update', { defaultValue: 'Update Password' })} | ||
</Button> | ||
</VStack> | ||
</form> | ||
) | ||
} | ||
|
||
export default PasswordForm |
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,32 @@ | ||
import { Avatar, Badge, Box, HStack, Text, VStack } from '@chakra-ui/react' | ||
import { UserRole } from '~src/queries/account' | ||
|
||
const Teams = ({ roles }: { roles: UserRole[] }) => ( | ||
<VStack spacing={4} align='stretch'> | ||
{roles.map((role, k) => ( | ||
<Box | ||
key={k} | ||
p={4} | ||
borderWidth='1px' | ||
borderRadius='lg' | ||
_hover={{ bg: 'gray.50', _dark: { bg: 'gray.700' } }} | ||
transition='background 0.2s' | ||
> | ||
<HStack spacing={4}> | ||
<Avatar size='md' src={role.organization.logo} name={role.organization.name} /> | ||
<Box flex='1'> | ||
<Text fontWeight='medium'>{role.organization.name}</Text> | ||
<Badge | ||
colorScheme={role.role === 'admin' ? 'purple' : role.role === 'owner' ? 'green' : 'blue'} | ||
fontSize='sm' | ||
> | ||
{role.role} | ||
</Badge> | ||
</Box> | ||
</HStack> | ||
</Box> | ||
))} | ||
</VStack> | ||
) | ||
|
||
export default Teams |
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
Oops, something went wrong.
cb58c01
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-app-stg.netlify.app as production
π Deployed on https://672102648c3a7601071f6fd9--vocdoni-app-stg.netlify.app
cb58c01
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-app-dev.netlify.app as production
π Deployed on https://672102660625ff0087adae87--vocdoni-app-dev.netlify.app