From cb58c01bc8b8539cfd499b1429b2a2894ae5bf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=92scar=20Casajuana?= Date: Tue, 29 Oct 2024 16:41:06 +0100 Subject: [PATCH] Account profile (#795) * 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 --- src/components/Account/Edit.tsx | 43 +++++++ src/components/Account/Form.tsx | 108 ++++++++++++++++ src/components/Account/Menu.tsx | 49 ++++++++ src/components/Account/PasswordForm.tsx | 116 ++++++++++++++++++ src/components/Account/Teams.tsx | 32 +++++ src/components/Auth/api.ts | 2 + src/components/Auth/useAuthProvider.ts | 8 +- src/components/Dashboard/Menu/Options.tsx | 2 +- .../{Account => Organization}/Create.tsx | 11 +- .../Organization/Dashboard/Create.tsx | 4 +- .../Organization/Dashboard/Votings.tsx | 2 +- .../EditProfile.tsx => Organization/Edit.tsx} | 16 +-- .../Layout.tsx => Organization/Form.tsx} | 0 .../ProcessCreate/Steps/AccountCreate.tsx | 2 +- src/constants/index.ts | 5 + src/elements/LayoutDashboard.tsx | 5 +- .../edit.tsx => organization.tsx} | 4 +- src/elements/dashboard/processes/index.tsx | 5 +- src/elements/dashboard/profile.tsx | 22 ++++ src/queries/account.ts | 79 +++++++++--- src/queries/organization.ts | 24 ++++ src/router/OrganizationProtectedRoute.tsx | 4 +- src/router/routes/dashboard.tsx | 13 +- src/router/routes/index.ts | 2 +- 24 files changed, 507 insertions(+), 51 deletions(-) create mode 100644 src/components/Account/Edit.tsx create mode 100644 src/components/Account/Form.tsx create mode 100644 src/components/Account/Menu.tsx create mode 100644 src/components/Account/PasswordForm.tsx create mode 100644 src/components/Account/Teams.tsx rename src/components/{Account => Organization}/Create.tsx (91%) rename src/components/{Account/EditProfile.tsx => Organization/Edit.tsx} (95%) rename src/components/{Account/Layout.tsx => Organization/Form.tsx} (100%) rename src/elements/dashboard/{organization/edit.tsx => organization.tsx} (84%) create mode 100644 src/elements/dashboard/profile.tsx create mode 100644 src/queries/organization.ts diff --git a/src/components/Account/Edit.tsx b/src/components/Account/Edit.tsx new file mode 100644 index 000000000..3385c8d2b --- /dev/null +++ b/src/components/Account/Edit.tsx @@ -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 ( + + + + + Profile + + + Password + + + Teams + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/Account/Form.tsx b/src/components/Account/Form.tsx new file mode 100644 index 000000000..4138eaa28 --- /dev/null +++ b/src/components/Account/Form.tsx @@ -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({ + 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 ( +
+ + + {t('profile.avatar.label', { defaultValue: 'Avatar' })} + + + + {t('avatar.hint', { defaultValue: 'Min 200x200px .PNG or .JPEG' })} + + + + + + {t('name.label', { defaultValue: 'Name' })} + + {errors.firstName?.message} + + + + {t('surname.label', { defaultValue: 'Surname' })} + + {errors.lastName?.message} + + + + {t('email.label', { defaultValue: 'Email' })} + + {errors.email?.message} + + + + +
+ ) +} + +export default AccountForm diff --git a/src/components/Account/Menu.tsx b/src/components/Account/Menu.tsx new file mode 100644 index 000000000..6d481eeb8 --- /dev/null +++ b/src/components/Account/Menu.tsx @@ -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 = (props) => { + const { logout } = useAuth() + const { data: profile, isLoading } = useProfile() + + if (isLoading) { + return ( + + + + ) + } + + return ( + + + + } + size='sm' + variant='outline' + aria-label='User menu' + /> + + + Profile + + + Logout + + + + + ) +} + +export default AccountMenu diff --git a/src/components/Account/PasswordForm.tsx b/src/components/Account/PasswordForm.tsx new file mode 100644 index 000000000..fecea983c --- /dev/null +++ b/src/components/Account/PasswordForm.tsx @@ -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({ + mutationFn: (params) => + bearedFetch(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() + + 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 ( +
+ + + {t('password.old.label', { defaultValue: 'Current Password' })} + + {errors.oldPassword?.message} + + + + {t('password.new.label', { defaultValue: 'New Password' })} + + {errors.newPassword?.message} + + + + {t('password.confirm.label', { defaultValue: 'Confirm Password' })} + + value === password || t('password.confirm.mismatch', { defaultValue: "Passwords don't match" }), + })} + /> + {errors.confirmPassword?.message} + + + + +
+ ) +} + +export default PasswordForm diff --git a/src/components/Account/Teams.tsx b/src/components/Account/Teams.tsx new file mode 100644 index 000000000..a35833bab --- /dev/null +++ b/src/components/Account/Teams.tsx @@ -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[] }) => ( + + {roles.map((role, k) => ( + + + + + {role.organization.name} + + {role.role} + + + + + ))} + +) + +export default Teams diff --git a/src/components/Auth/api.ts b/src/components/Auth/api.ts index baa19e6b1..5a525ab94 100644 --- a/src/components/Auth/api.ts +++ b/src/components/Auth/api.ts @@ -2,6 +2,8 @@ type MethodTypes = 'GET' | 'POST' | 'PUT' | 'DELETE' export enum ApiEndpoints { Login = 'auth/login', + Me = 'users/me', + Password = 'users/password', Refresh = 'auth/refresh', Register = 'users', Organizations = 'organizations', diff --git a/src/components/Auth/useAuthProvider.ts b/src/components/Auth/useAuthProvider.ts index 8d28ab14f..a168162fa 100644 --- a/src/components/Auth/useAuthProvider.ts +++ b/src/components/Auth/useAuthProvider.ts @@ -6,7 +6,7 @@ import { api, ApiEndpoints, ApiParams, UnauthorizedApiError } from '~components/ import { LoginResponse, useLogin, useRegister, useVerifyMail } from '~components/Auth/authQueries' enum LocalStorageKeys { - AUTH_TOKEN = 'authToken', + Token = 'authToken', } /** @@ -41,7 +41,7 @@ const useSigner = () => { export const useAuthProvider = () => { const { signer: clientSigner, clear } = useClient() - const [bearer, setBearer] = useState(localStorage.getItem(LocalStorageKeys.AUTH_TOKEN)) + const [bearer, setBearer] = useState(localStorage.getItem(LocalStorageKeys.Token)) const login = useLogin({ onSuccess: (data, variables) => { @@ -83,13 +83,13 @@ export const useAuthProvider = () => { ) const storeLogin = useCallback(({ token }: LoginResponse) => { - localStorage.setItem(LocalStorageKeys.AUTH_TOKEN, token) + localStorage.setItem(LocalStorageKeys.Token, token) setBearer(token) updateSigner(token) }, []) const logout = useCallback(() => { - localStorage.removeItem(LocalStorageKeys.AUTH_TOKEN) + localStorage.removeItem(LocalStorageKeys.Token) setBearer(null) clear() }, []) diff --git a/src/components/Dashboard/Menu/Options.tsx b/src/components/Dashboard/Menu/Options.tsx index 12a8c02ab..b99f03e09 100644 --- a/src/components/Dashboard/Menu/Options.tsx +++ b/src/components/Dashboard/Menu/Options.tsx @@ -53,7 +53,7 @@ export const DashboardMenuOptions = () => { label: t('settings'), icon: IoIosSettings, children: [ - { label: t('organization.organization'), route: '#organization' }, + { label: t('organization.organization'), route: Routes.dashboard.organization }, { label: t('team'), route: Routes.dashboard.team }, { label: t('billing'), route: '#billing' }, { label: t('subscription'), route: '#subscription' }, diff --git a/src/components/Account/Create.tsx b/src/components/Organization/Create.tsx similarity index 91% rename from src/components/Account/Create.tsx rename to src/components/Organization/Create.tsx index 90b6b15f0..6b2aac3f7 100644 --- a/src/components/Account/Create.tsx +++ b/src/components/Organization/Create.tsx @@ -7,12 +7,12 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { useClient } from '@vocdoni/react-providers' import { useState } from 'react' import { CreateOrgParams } from '~components/Account/AccountTypes' -import { PrivateOrgForm, PrivateOrgFormData, PublicOrgForm } from '~components/Account/Layout' import LogoutBtn from '~components/Account/LogoutBtn' import { useAccountCreate } from '~components/Account/useAccountCreate' import { ApiEndpoints } from '~components/Auth/api' import { useAuth } from '~components/Auth/useAuth' import FormSubmitMessage from '~components/Layout/FormSubmitMessage' +import { PrivateOrgForm, PrivateOrgFormData, PublicOrgForm } from './Form' type FormData = PrivateOrgFormData & CreateOrgParams @@ -25,16 +25,15 @@ type FormData = PrivateOrgFormData & CreateOrgParams // and the error is not thrown as an exception. const IgnoreAccountError = 'this user has not been assigned to any organization' -const useSaasAccountCreate = (options?: Omit, 'mutationFn'>) => { +const useOrganizationCreate = (options?: Omit, 'mutationFn'>) => { const { bearedFetch } = useAuth() return useMutation({ - mutationFn: (params: CreateOrgParams) => - bearedFetch(ApiEndpoints.Organizations, { body: params, method: 'POST' }), + mutationFn: (params: CreateOrgParams) => bearedFetch(ApiEndpoints.Organizations, { body: params, method: 'POST' }), ...options, }) } -export const AccountCreate = ({ children, ...props }: FlexProps) => { +export const OrganizationCreate = ({ children, ...props }: FlexProps) => { const { t } = useTranslation() const [isPending, setIsPending] = useState(false) @@ -45,7 +44,7 @@ export const AccountCreate = ({ children, ...props }: FlexProps) => { const { signer } = useClient() const { create: createAccount, error: accountError } = useAccountCreate() - const { mutateAsync: createSaasAccount, isError: isSaasError, error: saasError } = useSaasAccountCreate() + const { mutateAsync: createSaasAccount, isError: isSaasError, error: saasError } = useOrganizationCreate() const error = saasError || accountError diff --git a/src/components/Organization/Dashboard/Create.tsx b/src/components/Organization/Dashboard/Create.tsx index 78970a43e..990f370ad 100644 --- a/src/components/Organization/Dashboard/Create.tsx +++ b/src/components/Organization/Dashboard/Create.tsx @@ -1,6 +1,6 @@ import { Box, Button, Flex, Grid, Heading, Image, ListItem, Text, UnorderedList } from '@chakra-ui/react' import { Trans } from 'react-i18next' -import { AccountCreate } from '~components/Account/Create' +import { OrganizationCreate } from '../Create' import AuthBanner from './AuthBanner' import barca from '/assets/barca.png' import bellpuig from '/assets/bellpuig.svg.png' @@ -23,7 +23,7 @@ const CreateOrganization = () => { alignItems='center' pt={14} > - + diff --git a/src/components/Organization/Dashboard/Votings.tsx b/src/components/Organization/Dashboard/Votings.tsx index 0827ffb68..86845c224 100644 --- a/src/components/Organization/Dashboard/Votings.tsx +++ b/src/components/Organization/Dashboard/Votings.tsx @@ -1,7 +1,7 @@ import { Flex } from '@chakra-ui/react' import { RoutedPagination } from '@vocdoni/chakra-components' import { RoutedPaginationProvider, useOrganization, useRoutedPagination } from '@vocdoni/react-providers' -import { usePaginatedElections } from '~src/queries/account' +import { usePaginatedElections } from '~src/queries/organization' import { Routes } from '~src/router/routes' import ProcessesList from './ProcessesList' diff --git a/src/components/Account/EditProfile.tsx b/src/components/Organization/Edit.tsx similarity index 95% rename from src/components/Account/EditProfile.tsx rename to src/components/Organization/Edit.tsx index 9f9bbb3b8..dde4597ea 100644 --- a/src/components/Account/EditProfile.tsx +++ b/src/components/Organization/Edit.tsx @@ -9,7 +9,6 @@ import { BiTrash } from 'react-icons/bi' import { BsFillTrashFill } from 'react-icons/bs' import { MdBrowserUpdated } from 'react-icons/md' import { CreateOrgParams } from '~components/Account/AccountTypes' -import { PrivateOrgForm, PrivateOrgFormData, PublicOrgForm } from '~components/Account/Layout' import { useSaasAccount } from '~components/Account/useSaasAccount' import { ApiEndpoints } from '~components/Auth/api' import { useAuth } from '~components/Auth/useAuth' @@ -19,12 +18,13 @@ import { CustomizationTimeZoneSelector, SelectOptionType, } from '~components/Layout/SaasSelector' -import { REGEX_AVATAR } from '~constants' +import { InnerContentsMaxWidth, REGEX_AVATAR } from '~constants' +import { PrivateOrgForm, PrivateOrgFormData, PublicOrgForm } from './Form' import fallback from '/assets/default-avatar.png' type FormData = CustomOrgFormData & PrivateOrgFormData & CreateOrgParams -const useEditSaasOrganization = (options?: Omit, 'mutationFn'>) => { +const useOrganizationEdit = (options?: Omit, 'mutationFn'>) => { const { bearedFetch, signerAddress } = useAuth() return useMutation({ mutationFn: (params: CreateOrgParams) => @@ -36,7 +36,7 @@ const useEditSaasOrganization = (options?: Omit { +const EditOrganization = () => { const { t } = useTranslation() const { updateAccount, @@ -51,7 +51,7 @@ const EditProfile = () => { isError: isSaasError, error: saasError, isSuccess, - } = useEditSaasOrganization() + } = useOrganizationEdit() const methods = useForm({ defaultValues: { @@ -105,13 +105,13 @@ const EditProfile = () => { return ( - + { e.stopPropagation() @@ -234,4 +234,4 @@ const CustomizeOrgForm = () => { ) } -export default EditProfile +export default EditOrganization diff --git a/src/components/Account/Layout.tsx b/src/components/Organization/Form.tsx similarity index 100% rename from src/components/Account/Layout.tsx rename to src/components/Organization/Form.tsx diff --git a/src/components/ProcessCreate/Steps/AccountCreate.tsx b/src/components/ProcessCreate/Steps/AccountCreate.tsx index 1532d73db..a41db7a10 100644 --- a/src/components/ProcessCreate/Steps/AccountCreate.tsx +++ b/src/components/ProcessCreate/Steps/AccountCreate.tsx @@ -1,8 +1,8 @@ import { Text } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { Trans } from 'react-i18next' -import { AccountCreate as AccountCreationForm } from '~components/Account/Create' import { useFaucet } from '~components/Faucet/use-faucet' +import { OrganizationCreate as AccountCreationForm } from '~components/Organization/Create' const AccountCreate = () => { const [faucetAmount, setFaucetAmount] = useState(0) diff --git a/src/constants/index.ts b/src/constants/index.ts index c4ba80d65..e1703b2e9 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -6,6 +6,11 @@ export const FormatDateLong = 'MMMM d - yyyy H:mm' export const TokenPrice = 0.15 export const MinPurchaseTokens = 100 export const StripeEnabled = import.meta.env.STRIPE_PUBLIC_KEY !== '' +export const InnerContentsMaxWidth = { + base: 'full', + lg: '600px', + xl: '800px', +} const evocdoni = import.meta.env.VOCDONI_ENVIRONMENT diff --git a/src/elements/LayoutDashboard.tsx b/src/elements/LayoutDashboard.tsx index c19b4bef1..b9a06531d 100644 --- a/src/elements/LayoutDashboard.tsx +++ b/src/elements/LayoutDashboard.tsx @@ -4,6 +4,7 @@ import { OrganizationProvider, useClient } from '@vocdoni/react-providers' import { useState } from 'react' import { MdKeyboardArrowLeft } from 'react-icons/md' import { Outlet, Link as ReactRouterLink } from 'react-router-dom' +import AccountMenu from '~components/Account/Menu' import DashboardMenu from '~components/Dashboard/Menu' export type DashboardLayoutContext = { @@ -36,7 +37,7 @@ const LayoutDashboard: React.FC = () => { gap={6} > {/* Top Menu */} - + {back && ( { {title} )} + {/* User profile & menu */} + {/* Hamburger button to open sidebar on small screens */} } diff --git a/src/elements/dashboard/organization/edit.tsx b/src/elements/dashboard/organization.tsx similarity index 84% rename from src/elements/dashboard/organization/edit.tsx rename to src/elements/dashboard/organization.tsx index ad281007c..7feb7837a 100644 --- a/src/elements/dashboard/organization/edit.tsx +++ b/src/elements/dashboard/organization.tsx @@ -1,7 +1,7 @@ -import EditProfile from '~components/Account/EditProfile' import { useSaasAccount } from '~components/Account/useSaasAccount' import { DashboardContents } from '~components/Layout/Dashboard' import QueryDataLayout from '~components/Layout/QueryDataLayout' +import EditOrganization from '~components/Organization/Edit' const OrganizationEdit = () => { const { isLoading, isError, error } = useSaasAccount() @@ -9,7 +9,7 @@ const OrganizationEdit = () => { return ( - + ) diff --git a/src/elements/dashboard/processes/index.tsx b/src/elements/dashboard/processes/index.tsx index 098d53bb6..843ffffa8 100644 --- a/src/elements/dashboard/processes/index.tsx +++ b/src/elements/dashboard/processes/index.tsx @@ -7,12 +7,13 @@ import { DashboardLayoutContext } from '~elements/LayoutDashboard' const OrganizationVotings = () => { const { t } = useTranslation() - const { setTitle } = useOutletContext() + const { setBack, setTitle } = useOutletContext() // Set page title useEffect(() => { setTitle(t('organization.votings_list', { defaultValue: 'Voting processes list' })) - }, [setTitle]) + setBack(null) + }, [setTitle, setBack]) return ( diff --git a/src/elements/dashboard/profile.tsx b/src/elements/dashboard/profile.tsx new file mode 100644 index 000000000..74230870f --- /dev/null +++ b/src/elements/dashboard/profile.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useOutletContext } from 'react-router-dom' +import { AccountEdit } from '~components/Account/Edit' +import { DashboardContents } from '~components/Layout/Dashboard' +import { DashboardLayoutContext } from '~elements/LayoutDashboard' + +export const Profile = () => { + const { t } = useTranslation() + const { setTitle } = useOutletContext() + + // Set layout variables + useEffect(() => { + setTitle(t('profile')) + }, [setTitle]) + + return ( + + + + ) +} diff --git a/src/queries/account.ts b/src/queries/account.ts index 21c186899..764fe5239 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -1,24 +1,67 @@ -import { useQuery } from '@tanstack/react-query' -import { useClient } from '@vocdoni/react-providers' - -export const useLatestElections = (limit = 5) => { - const { client, account } = useClient() - - return useQuery({ - enabled: !!account?.address, - queryKey: ['organization', 'elections', account?.address, 0], - queryFn: async () => client.fetchElections({ organizationId: account?.address, page: 0, limit }), - select: (data) => data.elections, - retry: false, +import { DefinedInitialDataOptions, QueryKey, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { ApiEndpoints } from '~components/Auth/api' +import { useAuth } from '~components/Auth/useAuth' + +export interface Organization { + address: string + name: string + type: string + description: string + size: number + color: string + logo: string + subdomain: string + timezone: string + active: boolean + parent?: any +} + +export interface UserRole { + role: string + organization: Organization +} + +export interface User { + email: string + firstName: string + lastName: string + organizations: Array +} + +export interface UpdateProfileParams { + firstName: string + lastName: string +} + +export interface AuthResponse { + token: string + expirity: string +} + +export const useProfile = ( + options?: Omit, 'queryKey' | 'queryFn'> +) => { + const { bearedFetch } = useAuth() + + return useQuery({ + ...options, + queryKey: ['profile'], + queryFn: () => bearedFetch(ApiEndpoints.Me), }) } -export const usePaginatedElections = (page: number) => { - const { client, account } = useClient() +export const useUpdateProfile = () => { + const { bearedFetch } = useAuth() + const queryClient = useQueryClient() - return useQuery({ - enabled: !!account?.address, - queryKey: ['organization', 'elections', account?.address, page], - queryFn: async () => client.fetchElections({ organizationId: account?.address, page }), + return useMutation({ + mutationFn: (body) => + bearedFetch(ApiEndpoints.Me, { + method: 'PUT', + body, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['profile'] }) + }, }) } diff --git a/src/queries/organization.ts b/src/queries/organization.ts new file mode 100644 index 000000000..21c186899 --- /dev/null +++ b/src/queries/organization.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query' +import { useClient } from '@vocdoni/react-providers' + +export const useLatestElections = (limit = 5) => { + const { client, account } = useClient() + + return useQuery({ + enabled: !!account?.address, + queryKey: ['organization', 'elections', account?.address, 0], + queryFn: async () => client.fetchElections({ organizationId: account?.address, page: 0, limit }), + select: (data) => data.elections, + retry: false, + }) +} + +export const usePaginatedElections = (page: number) => { + const { client, account } = useClient() + + return useQuery({ + enabled: !!account?.address, + queryKey: ['organization', 'elections', account?.address, page], + queryFn: async () => client.fetchElections({ organizationId: account?.address, page }), + }) +} diff --git a/src/router/OrganizationProtectedRoute.tsx b/src/router/OrganizationProtectedRoute.tsx index 485bdf09b..e4a9be5e2 100644 --- a/src/router/OrganizationProtectedRoute.tsx +++ b/src/router/OrganizationProtectedRoute.tsx @@ -2,7 +2,7 @@ import { useClient } from '@vocdoni/react-providers' import { Navigate, Outlet } from 'react-router-dom' import { useAccountHealthTools } from '~components/Account/use-account-health-tools' import { useAuth } from '~components/Auth/useAuth' -import CreateOrganizationSaas from '~components/Organization/Dashboard/Create' +import CreateOrganization from '~components/Organization/Dashboard/Create' import { Loading } from '~src/router/SuspenseLoader' import { Routes } from './routes' @@ -23,7 +23,7 @@ const OrganizationProtectedRoute = () => { } if (!exists && !signerAddress) { - return + return } return diff --git a/src/router/routes/dashboard.tsx b/src/router/routes/dashboard.tsx index 4cfd5a1ab..8f417dc2b 100644 --- a/src/router/routes/dashboard.tsx +++ b/src/router/routes/dashboard.tsx @@ -3,6 +3,7 @@ import { VocdoniSDKClient } from '@vocdoni/sdk' import { lazy } from 'react' // These aren't lazy loaded since they are main layouts and related components import { Params } from 'react-router-dom' +import { Profile } from '~elements/dashboard/profile' import Error from '~elements/Error' import LayoutDashboard from '~elements/LayoutDashboard' import { Routes } from '.' @@ -10,7 +11,7 @@ import OrganizationProtectedRoute from '../OrganizationProtectedRoute' import { SuspenseLoader } from '../SuspenseLoader' // elements/pages -const OrganizationEdit = lazy(() => import('~elements/dashboard/organization/edit')) +const OrganizationEdit = lazy(() => import('~elements/dashboard/organization')) const DashboardProcesses = lazy(() => import('~elements/dashboard/processes')) const DashboardProcessView = lazy(() => import('~elements/dashboard/processes/view')) const OrganizationTeam = lazy(() => import('~elements/dashboard/team')) @@ -45,13 +46,21 @@ const DashboardElements = (client: VocdoniSDKClient) => [ errorElement: , }, { - path: Routes.dashboard.profile, + path: Routes.dashboard.organization, element: ( ), }, + { + path: Routes.dashboard.profile, + element: ( + + + + ), + }, { path: Routes.dashboard.processes, element: ( diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index f565b3fa9..c04eecea4 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -12,7 +12,7 @@ export const Routes = { organization: '/admin/organization', process: '/admin/process/:id', processes: '/admin/processes/:page?/:status?', - profile: '/admin/organization/edit', + profile: '/admin/profile', team: '/admin/team', }, faucet: '/faucet',