ajout app

This commit is contained in:
2024-04-17 20:22:30 +02:00
parent cc017cfc5e
commit f9d05a2fd3
8025 changed files with 729805 additions and 0 deletions

View 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

View 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

View 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