ajout app
This commit is contained in:
160
Voltaserve/ui/src/pages/account/account-invitations-page.tsx
Normal file
160
Voltaserve/ui/src/pages/account/account-invitations-page.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import InvitationAPI, { SortBy, SortOrder } from '@/client/api/invitation'
|
||||
import UserAPI from '@/client/idp/user'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import prettyDate from '@/helpers/pretty-date'
|
||||
import userToString from '@/helpers/user-to-string'
|
||||
import { incomingInvitationPaginationStorage } from '@/infra/pagination'
|
||||
import {
|
||||
IconMoreVert,
|
||||
SectionSpinner,
|
||||
PagePagination,
|
||||
usePagePagination,
|
||||
} from '@/lib'
|
||||
|
||||
const AccountInvitationsPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const toast = useToast()
|
||||
const { data: user, error: userError } = UserAPI.useGet()
|
||||
const { page, size, steps, setPage, setSize } = usePagePagination({
|
||||
navigate,
|
||||
location,
|
||||
storage: incomingInvitationPaginationStorage(),
|
||||
})
|
||||
const {
|
||||
data: list,
|
||||
error: invitationsError,
|
||||
mutate,
|
||||
} = InvitationAPI.useGetIncoming(
|
||||
{ page, size, sortBy: SortBy.DateCreated, sortOrder: SortOrder.Desc },
|
||||
swrConfig(),
|
||||
)
|
||||
|
||||
const handleAccept = useCallback(
|
||||
async (invitationId: string) => {
|
||||
await InvitationAPI.accept(invitationId)
|
||||
mutate()
|
||||
toast({
|
||||
title: 'Invitation accepted',
|
||||
status: 'success',
|
||||
isClosable: true,
|
||||
})
|
||||
},
|
||||
[mutate, toast],
|
||||
)
|
||||
|
||||
const handleDecline = useCallback(
|
||||
async (invitationId: string) => {
|
||||
await InvitationAPI.decline(invitationId)
|
||||
mutate()
|
||||
toast({
|
||||
title: 'Invitation declined',
|
||||
status: 'info',
|
||||
isClosable: true,
|
||||
})
|
||||
},
|
||||
[mutate, toast],
|
||||
)
|
||||
|
||||
if (userError || invitationsError) {
|
||||
return null
|
||||
}
|
||||
if (!user || !list) {
|
||||
return <SectionSpinner />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{user.fullName}</title>
|
||||
</Helmet>
|
||||
{list.data.length === 0 && (
|
||||
<div
|
||||
className={cx('flex', 'items-center', 'justify-center', 'h-[300px]')}
|
||||
>
|
||||
<span>There are no invitations.</span>
|
||||
</div>
|
||||
)}
|
||||
{list.data.length > 0 && (
|
||||
<div className={cx('flex', 'flex-col', 'gap-3.5', 'pb-3.5')}>
|
||||
<Table variant="simple">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>From</Th>
|
||||
<Th>Organization</Th>
|
||||
<Th>Date</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{list.data.length > 0 &&
|
||||
list.data.map((i) => (
|
||||
<Tr key={i.id}>
|
||||
<Td>{userToString(i.owner)}</Td>
|
||||
<Td>{i.organization.name}</Td>
|
||||
<Td>{prettyDate(i.createTime)}</Td>
|
||||
<Td className={cx('text-right')}>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<IconMoreVert />}
|
||||
variant="ghost"
|
||||
aria-label=""
|
||||
/>
|
||||
<Portal>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => handleAccept(i.id)}>
|
||||
Accept
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={cx('text-red-500')}
|
||||
onClick={() => handleDecline(i.id)}
|
||||
>
|
||||
Decline
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{list && (
|
||||
<PagePagination
|
||||
style={{ alignSelf: 'end' }}
|
||||
totalElements={list.totalElements}
|
||||
totalPages={list.totalPages}
|
||||
page={page}
|
||||
size={size}
|
||||
steps={steps}
|
||||
setPage={setPage}
|
||||
setSize={setSize}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountInvitationsPage
|
||||
118
Voltaserve/ui/src/pages/account/account-layout.tsx
Normal file
118
Voltaserve/ui/src/pages/account/account-layout.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Heading,
|
||||
IconButton,
|
||||
Tab,
|
||||
TabList,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import NotificationAPI from '@/client/api/notification'
|
||||
import UserAPI from '@/client/idp/user'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import AccountEditPicture from '@/components/account/edit-picture'
|
||||
import { IconEdit } from '@/lib'
|
||||
|
||||
const AccountLayout = () => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState(false)
|
||||
const { data: user } = UserAPI.useGet(swrConfig())
|
||||
const { data: notfications } = NotificationAPI.useGetAll(swrConfig())
|
||||
const invitationCount = useMemo(
|
||||
() => notfications?.filter((e) => e.type === 'new_invitation').length,
|
||||
[notfications],
|
||||
)
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const segments = location.pathname.split('/')
|
||||
const segment = segments[segments.length - 1]
|
||||
if (segment === 'settings') {
|
||||
setTabIndex(0)
|
||||
} else if (segment === 'invitation') {
|
||||
setTabIndex(1)
|
||||
}
|
||||
}, [location])
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-row', 'gap-2.5')}>
|
||||
<div
|
||||
className={cx('flex', 'flex-col', 'gap-2', 'items-center', 'w-[250px]')}
|
||||
>
|
||||
<div className={cx('flex', 'flex-col', 'gap-2', 'items-center')}>
|
||||
<div className={cx('relative', 'shrink-0')}>
|
||||
<Avatar
|
||||
name={user.fullName}
|
||||
src={user.picture}
|
||||
size="2xl"
|
||||
className={cx('w-[165px]', 'h-[165px]')}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<IconEdit />}
|
||||
variant="solid-gray"
|
||||
right="5px"
|
||||
bottom="10px"
|
||||
position="absolute"
|
||||
zIndex={1000}
|
||||
aria-label=""
|
||||
onClick={() => setIsImageModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<Heading className={cx('text-center', 'text-heading')}>
|
||||
{user.fullName}
|
||||
</Heading>
|
||||
</div>
|
||||
<div className={cx('w-full', 'gap-1')}>
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
type="submit"
|
||||
className={cx('w-full')}
|
||||
onClick={() => navigate('/sign-out')}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('w-full', 'pb-1.5')}>
|
||||
<Tabs
|
||||
variant="solid-rounded"
|
||||
colorScheme="gray"
|
||||
index={tabIndex}
|
||||
className={cx('pb-2.5')}
|
||||
>
|
||||
<TabList>
|
||||
<Tab onClick={() => navigate('/account/settings')}>Settings</Tab>
|
||||
<Tab onClick={() => navigate('/account/invitation')}>
|
||||
<div
|
||||
className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')}
|
||||
>
|
||||
<span>Invitations</span>
|
||||
{invitationCount && invitationCount > 0 ? (
|
||||
<Tag className={cx('rounded-full')}>{invitationCount}</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Outlet />
|
||||
</div>
|
||||
<AccountEditPicture
|
||||
open={isImageModalOpen}
|
||||
user={user}
|
||||
onClose={() => setIsImageModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountLayout
|
||||
195
Voltaserve/ui/src/pages/account/account-settings-page.tsx
Normal file
195
Voltaserve/ui/src/pages/account/account-settings-page.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Divider,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
Progress,
|
||||
Switch,
|
||||
Tooltip,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import StorageAPI from '@/client/api/storage'
|
||||
import UserAPI from '@/client/idp/user'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import AccountChangePassword from '@/components/account/account-change-password'
|
||||
import AccountDelete from '@/components/account/account-delete'
|
||||
import AccountEditEmail from '@/components/account/account-edit-email'
|
||||
import AccountEditFullName from '@/components/account/account-edit-full-name'
|
||||
import prettyBytes from '@/helpers/pretty-bytes'
|
||||
import { IconEdit, IconDelete, SectionSpinner, IconWarning } from '@/lib'
|
||||
|
||||
const EditButton = (props: IconButtonProps) => (
|
||||
<IconButton
|
||||
icon={<IconEdit />}
|
||||
className={cx('h-[40px]', 'w-[40px]')}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const Spacer = () => <div className={cx('grow')} />
|
||||
|
||||
const AccountSettingsPage = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode()
|
||||
const { data: user, error: userError } = UserAPI.useGet()
|
||||
const { data: storageUsage, error: storageUsageError } =
|
||||
StorageAPI.useGetAccountUsage(swrConfig())
|
||||
const [isFullNameModalOpen, setIsFullNameModalOpen] = useState(false)
|
||||
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false)
|
||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const sectionClassName = cx('flex', 'flex-col', 'gap-1', 'py-1.5')
|
||||
const rowClassName = cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-1',
|
||||
`h-[40px]`,
|
||||
)
|
||||
|
||||
if (userError) {
|
||||
return null
|
||||
}
|
||||
if (!user) {
|
||||
return <SectionSpinner />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{user.fullName}</title>
|
||||
</Helmet>
|
||||
<div className={cx('flex', 'flex-col', 'gap-0')}>
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Storage Usage</span>
|
||||
{storageUsageError && <span>Failed to load storage usage.</span>}
|
||||
{storageUsage && !storageUsageError && (
|
||||
<>
|
||||
<span>
|
||||
{prettyBytes(storageUsage.bytes)} of{' '}
|
||||
{prettyBytes(storageUsage.maxBytes)} used
|
||||
</span>
|
||||
<Progress value={storageUsage.percentage} hasStripe />
|
||||
</>
|
||||
)}
|
||||
{!storageUsage && !storageUsageError && (
|
||||
<>
|
||||
<span>Calculating…</span>
|
||||
<Progress value={0} hasStripe />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Basics</span>
|
||||
<div className={cx(rowClassName)}>
|
||||
<span>Full name</span>
|
||||
<Spacer />
|
||||
<span>{user.fullName}</span>
|
||||
<EditButton
|
||||
aria-label=""
|
||||
onClick={() => {
|
||||
setIsFullNameModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Credentials</span>
|
||||
<div className={cx(rowClassName)}>
|
||||
<span>Email</span>
|
||||
<Spacer />
|
||||
{user.pendingEmail && (
|
||||
<div
|
||||
className={cx('flex', 'flex-row', 'gap-0.5', 'items-center')}
|
||||
>
|
||||
<Tooltip label="Please check your inbox to confirm your email.">
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'cursor-default',
|
||||
)}
|
||||
>
|
||||
<IconWarning className={cx('text-yellow-400')} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span>{user.pendingEmail}</span>
|
||||
</div>
|
||||
)}
|
||||
{!user.pendingEmail && (
|
||||
<span>{user.pendingEmail || user.email}</span>
|
||||
)}
|
||||
<EditButton
|
||||
aria-label=""
|
||||
onClick={() => {
|
||||
setIsEmailModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx(rowClassName)}>
|
||||
<span>Password</span>
|
||||
<Spacer />
|
||||
<EditButton
|
||||
aria-label=""
|
||||
onClick={() => {
|
||||
setIsPasswordModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Theme</span>
|
||||
<div className={cx(rowClassName)}>
|
||||
<span>Dark mode</span>
|
||||
<Spacer />
|
||||
<Switch
|
||||
isChecked={colorMode === 'dark'}
|
||||
onChange={() => toggleColorMode()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Advanced</span>
|
||||
<div className={cx(rowClassName)}>
|
||||
<span>Delete account</span>
|
||||
<Spacer />
|
||||
<IconButton
|
||||
icon={<IconDelete />}
|
||||
variant="solid"
|
||||
colorScheme="red"
|
||||
aria-label=""
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AccountEditFullName
|
||||
open={isFullNameModalOpen}
|
||||
user={user}
|
||||
onClose={() => setIsFullNameModalOpen(false)}
|
||||
/>
|
||||
<AccountEditEmail
|
||||
open={isEmailModalOpen}
|
||||
user={user}
|
||||
onClose={() => setIsEmailModalOpen(false)}
|
||||
/>
|
||||
<AccountChangePassword
|
||||
open={isPasswordModalOpen}
|
||||
user={user}
|
||||
onClose={() => setIsPasswordModalOpen(false)}
|
||||
/>
|
||||
<AccountDelete
|
||||
open={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSettingsPage
|
||||
Reference in New Issue
Block a user