Merge branch 'dev' into staging

This commit is contained in:
Max S 2024-07-24 18:12:51 +02:00
commit 0b1305d09a
28 changed files with 652 additions and 725 deletions

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8569 17.0817C16.7514 16.857 18.5783 16.4116 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919C17.9999 9.03281 18 9.01641 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9L5.9998 9.75C5.9998 12.0656 5.12527 14.177 3.68848 15.7719C5.4214 16.4116 7.24843 16.857 9.14314 17.0818M14.8569 17.0817C13.92 17.1928 12.9666 17.25 11.9998 17.25C11.0332 17.25 10.0799 17.1929 9.14314 17.0818M14.8569 17.0817C14.9498 17.3711 15 17.6797 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18C9 17.6797 9.05019 17.3712 9.14314 17.0818" stroke="#47535D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="17.5" cy="6.5" r="4.5" fill="#FF4617"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

View File

@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import { IAppRule } from "@Front/Api/Entities/rule"; import { IAppRule } from "@Front/Api/Entities/rule";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { IHeaderLinkProps } from "../../../HeaderLink"; import { IHeaderLinkProps } from "../../../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink"; import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink";
import useToggle from "@Front/Hooks/useToggle"; import useToggle from "@Front/Hooks/useToggle";

View File

@ -1,9 +1,4 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
.burger-icon {
cursor: pointer;
height: 24px;
width: 24px;
}
} }

View File

@ -1,35 +1,37 @@
import React from "react"; import useOpenable from "@Front/Hooks/useOpenable";
import classes from "./classes.module.scss"; import { Bars3BottomRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
import Image from "next/image"; import React, { useEffect } from "react";
import BurgerIcon from "@Assets/Icons/burger.svg";
import CrossIcon from "@Assets/Icons/cross.svg"; import IconButton from "../../IconButton";
import BurgerModal from "./BurgerModal"; import BurgerModal from "./BurgerModal";
import classes from "./classes.module.scss";
import WindowStore from "@Front/Stores/WindowStore";
type IProps = { const headerBreakpoint = 1023;
isModalOpen: boolean;
openBurgerMenu: () => void;
closeBurgerMenu: () => void;
};
type IState = {
// isModalOpen: boolean;
};
export default class BurgerMenu extends React.Component<IProps, IState> { export default function BurgerMenu() {
constructor(props: IProps) { const { isOpen, toggle, close } = useOpenable();
super(props);
} useEffect(() => {
const onResize = (window: Window) => {
if (window.innerWidth > headerBreakpoint && isOpen) close();
};
const windowResizeSubscription = WindowStore.getInstance().onResize((window) => onResize(window));
return () => {
windowResizeSubscription();
};
}, [close, isOpen]);
public override render(): JSX.Element {
return ( return (
<div className={classes["root"]}> <div className={classes["root"]}>
<Image {isOpen ? (
alt="burger" <IconButton icon={<XMarkIcon />} onClick={toggle} />
src={this.props.isModalOpen ? CrossIcon : BurgerIcon} ) : (
className={classes["burger-icon"]} <IconButton icon={<Bars3BottomRightIcon />} onClick={toggle} />
onClick={this.props.openBurgerMenu} )}
/> <BurgerModal isOpen={isOpen} closeModal={close} />
{this.props.isModalOpen && <BurgerModal isOpen={this.props.isModalOpen} closeModal={this.props.closeBurgerMenu} />}
</div> </div>
); );
}
} }

View File

@ -0,0 +1,27 @@
@import "@Themes/constants.scss";
.root {
position: relative;
align-self: center;
display: flex;
justify-content: center;
align-items: center;
height: fit-content;
padding: var(--spacing-lg, 24px) var(--spacing-md, 16px);
border-radius: var(--menu-radius, 0px);
border-bottom: 1px solid var(--menu-border, #d7dce0);
background: var(--button-header-background, #fff);
user-select: none;
cursor: pointer;
&.active {
border-bottom: 2px solid var(--button-header-border, #ff4617);
}
&.desactivated {
cursor: not-allowed;
}
}

View File

@ -0,0 +1,71 @@
import classNames from "classnames";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import useHoverable from "@Front/Hooks/useHoverable";
export type IHeaderLinkProps = {
text: string | JSX.Element;
path: string;
routesActive?: string[];
disabledLink?: boolean;
};
export default function ButtonHeader(props: IHeaderLinkProps) {
const { disabledLink, path, text, routesActive } = props;
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = React.useState(path === pathname);
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable();
useEffect(() => {
if (path === pathname) setIsActive(true);
if (routesActive) {
for (const routeActive of routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) setIsActive(true);
}
}
}, [isActive, pathname, path, routesActive]);
if (path !== "" && path !== undefined && !disabledLink) {
return (
<Link
href={path}
className={classNames(classes["root"], isActive && classes["active"])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<Typography
typo={isActive ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isHovered ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_700}>
{text}
</Typography>
</Link>
);
}
if (disabledLink) {
return (
<div
className={classNames(classes["root"], isActive && classes["active"])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<Typography
typo={isActive ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isHovered ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_700}>
{text}
</Typography>
</div>
);
}
return (
<div className={classNames(classes["root"], classes["desactivated"])}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{text}
</Typography>
</div>
);
}

View File

@ -1,30 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: flex;
position: relative;
width: fit-content;
margin: auto;
height: 83px;
padding: 10px 16px;
.content {
margin: auto;
}
.underline {
width: 100%;
height: 3px;
background-color: var(--color-generic-white);
position: absolute;
bottom: 0;
left: 0;
&[data-active="true"] {
background-color: var(--color-generic-black);
}
}
&.desactivated {
cursor: not-allowed;
}
}

View File

@ -1,60 +0,0 @@
import classNames from "classnames";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import useHoverable from "@Front/Hooks/useHoverable";
export type IHeaderLinkProps = {
text: string | JSX.Element;
path: string;
routesActive?: string[];
};
export default function HeaderLink(props: IHeaderLinkProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = React.useState(props.path === pathname);
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable();
useEffect(() => {
if (props.path === pathname) setIsActive(true);
if (props.routesActive) {
for (const routeActive of props.routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) setIsActive(true);
}
}
}, [isActive, pathname, props.path, props.routesActive]);
if (props.path !== "" && props.path !== undefined) {
return (
<Link
href={props.path}
className={classNames(classes["root"], isActive && classes["active"])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className={classes["content"]}>
<Typography
typo={isActive || isHovered ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isHovered ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
</div>
<div className={classes["underline"]} data-active={(isActive || isHovered).toString()} />
</Link>
);
} else {
return (
<div className={classNames(classes["root"], classes["desactivated"])}>
<div className={classes["content"]}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
</div>
</div>
);
}
}

View File

@ -1,7 +1,7 @@
import classNames from "classnames"; import classNames from "classnames";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { IHeaderLinkProps } from "../HeaderLink"; import { IHeaderLinkProps } from "../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "../../Typography"; import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";

View File

@ -2,7 +2,5 @@
.root { .root {
display: inline-flex; display: inline-flex;
@media screen and (max-width: $screen-ls) { align-items: center;
display: none;
}
} }

View File

@ -1,15 +1,17 @@
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import Notifications from "@Front/Api/LeCoffreApi/Notary/Notifications/Notifications";
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import Toasts from "@Front/Stores/Toasts";
import { AdjustmentsVerticalIcon, BanknotesIcon, Square3Stack3DIcon, TagIcon, UsersIcon } from "@heroicons/react/24/outline";
import { usePathname } from "next/navigation";
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import HeaderLink from "../HeaderLink"; import Menu, { IItem } from "../../Menu";
import ButtonHeader from "../ButtonHeader";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import { usePathname } from "next/navigation";
import Notifications from "@Front/Api/LeCoffreApi/Notary/Notifications/Notifications";
import Toasts from "@Front/Stores/Toasts";
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
import HeaderSubmenu from "../HeaderSubmenu";
export default function Navigation() { export default function Navigation() {
const pathname = usePathname(); const pathname = usePathname();
@ -73,7 +75,7 @@ export default function Navigation() {
}, },
]}> ]}>
<> <>
<HeaderLink <ButtonHeader
text={"Dossiers en cours"} text={"Dossiers en cours"}
path={Module.getInstance().get().modules.pages.Folder.props.path} path={Module.getInstance().get().modules.pages.Folder.props.path}
routesActive={[ routesActive={[
@ -81,19 +83,47 @@ export default function Navigation() {
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path, Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]} ]}
/> />
<HeaderLink <ButtonHeader
text={"Dossiers archivés"} text={"Dossiers archivés"}
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path} path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]} routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/> />
<Menu items={officeItems} openingSide="center" openOnHover>
<ButtonHeader
text={"Espace office"}
path={Module.getInstance().get().modules.pages.Collaborators.props.path}
routesActive={officeItems.flatMap((item) => item.routesActive ?? [])}
disabledLink
/>
</Menu>
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
]}>
<Menu items={superAdminItems} openingSide="center" openOnHover>
<ButtonHeader
text={"Espace super admin"}
path={Module.getInstance().get().modules.pages.Collaborators.props.path}
routesActive={officeItems.flatMap((item) => item.routesActive ?? [])}
disabledLink
/>
</Menu>
</Rules>
</> </>
</Rules> </Rules>
<HeaderSubmenu </div>
text={"Espace office"} );
links={[ }
const officeItems: IItem[] = [
{ {
icon: <UsersIcon />,
text: "Collaborateurs", text: "Collaborateurs",
path: Module.getInstance().get().modules.pages.Collaborators.props.path, link: Module.getInstance().get().modules.pages.Collaborators.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path, Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
Module.getInstance().get().modules.pages.Collaborators.props.path, Module.getInstance().get().modules.pages.Collaborators.props.path,
@ -106,8 +136,9 @@ export default function Navigation() {
], ],
}, },
{ {
icon: <AdjustmentsVerticalIcon />,
text: "Gestion des rôles", text: "Gestion des rôles",
path: Module.getInstance().get().modules.pages.Roles.props.path, link: Module.getInstance().get().modules.pages.Roles.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.Roles.pages.Create.props.path, Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path, Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
@ -121,8 +152,9 @@ export default function Navigation() {
], ],
}, },
{ {
icon: <Square3Stack3DIcon />,
text: "Paramétrage des listes de pièces", text: "Paramétrage des listes de pièces",
path: Module.getInstance().get().modules.pages.DeedTypes.props.path, link: Module.getInstance().get().modules.pages.DeedTypes.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path, Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path, Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
@ -140,8 +172,9 @@ export default function Navigation() {
], ],
}, },
{ {
icon: <BanknotesIcon />,
text: "RIB Office", text: "RIB Office",
path: Module.getInstance().get().modules.pages.OfficesRib.props.path, link: Module.getInstance().get().modules.pages.OfficesRib.props.path,
rules: [ rules: [
{ {
action: AppRuleActions.update, action: AppRuleActions.update,
@ -150,8 +183,9 @@ export default function Navigation() {
], ],
}, },
{ {
icon: <TagIcon />,
text: "Abonnement", text: "Abonnement",
path: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path, link: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path, Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path, Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
@ -168,23 +202,13 @@ export default function Navigation() {
}, },
], ],
}, },
]} ];
/>
{/* </Rules> */} const superAdminItems: IItem[] = [
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
]}>
<HeaderSubmenu
text={"Espace super admin"}
links={[
{ {
icon: <UsersIcon />,
text: "Gestion des utilisateurs", text: "Gestion des utilisateurs",
path: Module.getInstance().get().modules.pages.Users.props.path, link: Module.getInstance().get().modules.pages.Users.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path, Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
Module.getInstance().get().modules.pages.Users.props.path, Module.getInstance().get().modules.pages.Users.props.path,
@ -197,8 +221,9 @@ export default function Navigation() {
], ],
}, },
{ {
icon: <UsersIcon />,
text: "Gestion des offices", text: "Gestion des offices",
path: Module.getInstance().get().modules.pages.Offices.props.path, link: Module.getInstance().get().modules.pages.Offices.props.path,
routesActive: [ routesActive: [
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path, Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
Module.getInstance().get().modules.pages.Offices.props.path, Module.getInstance().get().modules.pages.Offices.props.path,
@ -210,9 +235,4 @@ export default function Navigation() {
}, },
], ],
}, },
]} ];
/>
</Rules>
</div>
);
}

View File

@ -1,18 +1,4 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
.icon-container {
position: relative;
cursor: pointer;
.notification-icon {
height: 24px;
width: 24px;
}
.notification-dot {
position: absolute;
top: 0;
right: 0;
}
}
} }

View File

@ -1,63 +1,54 @@
import InfoIcon from "@Assets/Icons/info.svg"; import useOpenable from "@Front/Hooks/useOpenable";
import NotificationIcon from "@Assets/Icons/notification.svg";
import Toasts, { IToast } from "@Front/Stores/Toasts"; import Toasts, { IToast } from "@Front/Stores/Toasts";
import Image from "next/image"; import { BellIcon } from "@heroicons/react/24/outline";
import React from "react"; import React, { useEffect, useState } from "react";
import IconButton from "../../IconButton";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import NotificationModal from "./NotificationModal"; import NotificationModal from "./NotificationModal";
type IProps = { export default function Notifications() {
isModalOpen: boolean; const [_toastList, setToastList] = useState<IToast[] | null>(Toasts.getInstance().toasts);
openNotificationModal: () => void; const [hasNotifications, setHasNotifications] = useState<boolean>(Toasts.getInstance().toasts.length > 0);
closeNotificationModal: () => void; const { isOpen, close, toggle } = useOpenable();
};
type IState = {
hasNotifications: boolean;
toastList: IToast[] | null;
};
export default class Notifications extends React.Component<IProps, IState> { useEffect(() => {
private removeOnToastChange: () => void = () => {}; const handleToastChange = (newToastList: IToast[] | null) => {
setToastList(newToastList);
constructor(props: IProps) { setHasNotifications(newToastList ? newToastList.length > 0 : false);
super(props);
this.state = {
toastList: Toasts.getInstance().toasts, //TODO : Get from bbd
hasNotifications: Toasts.getInstance().toasts.length > 0, // TODO: Change this when we have notification stored in bbd, unread notifications
}; };
this.handleToastChange = this.handleToastChange.bind(this);
}
public override render(): JSX.Element { const removeOnToastChange = Toasts.getInstance().onChange(handleToastChange);
return () => {
removeOnToastChange();
};
}, []);
return ( return (
<div className={classes["root"]}> <div className={classes["root"]}>
<div className={classes["icon-container"]} onClick={this.props.openNotificationModal}> {hasNotifications ? (
<Image alt="notifications" src={NotificationIcon} className={classes["notification-icon"]} /> <IconButton icon={<BellNotifIcon />} onClick={toggle} />
{this.state.hasNotifications && ( ) : (
<Image className={classes["notification-dot"]} src={InfoIcon} alt="Unread notification" /> <IconButton icon={<BellIcon />} onClick={toggle} />
)} )}
</div>
{this.props.isModalOpen && ( <NotificationModal isOpen={isOpen} closeModal={close} />
<NotificationModal isOpen={this.props.isModalOpen} closeModal={this.props.closeNotificationModal} />
)}
</div> </div>
); );
} }
public override componentDidMount() { function BellNotifIcon() {
this.removeOnToastChange = Toasts.getInstance().onChange(this.handleToastChange); return (
} <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
public override componentWillUnmount() { d="M14.8569 17.0817C16.7514 16.857 18.5783 16.4116 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919C17.9999 9.03281 18 9.01641 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9L5.9998 9.75C5.9998 12.0656 5.12527 14.177 3.68848 15.7719C5.4214 16.4116 7.24843 16.857 9.14314 17.0818M14.8569 17.0817C13.92 17.1928 12.9666 17.25 11.9998 17.25C11.0332 17.25 10.0799 17.1929 9.14314 17.0818M14.8569 17.0817C14.9498 17.3711 15 17.6797 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18C9 17.6797 9.05019 17.3712 9.14314 17.0818"
this.removeOnToastChange(); stroke="#47535D"
} stroke-width="1.5"
stroke-linecap="round"
private handleToastChange(toastList: IToast[] | null) { stroke-linejoin="round"
this.setState({ />
toastList, <circle cx="17.5" cy="6.5" r="4.5" fill="#FF4617" />
hasNotifications: toastList ? toastList.length > 0 : false, </svg>
}); );
}
} }

View File

@ -1,9 +1,5 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
.profile-icon {
cursor: pointer;
height: 24px;
width: 24px;
}
} }

View File

@ -1,30 +1,33 @@
import React from "react"; import useOpenable from "@Front/Hooks/useOpenable";
import WindowStore from "@Front/Stores/WindowStore";
import { UserIcon } from "@heroicons/react/24/outline";
import React, { useEffect } from "react";
import IconButton from "../../IconButton";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import Image from "next/image";
import ProfileIcon from "@Assets/Icons/user.svg";
import ProfileModal from "./ProfileModal"; import ProfileModal from "./ProfileModal";
type IProps = { const headerBreakpoint = 1023;
isModalOpen: boolean;
openProfileModal: () => void;
closeProfileModal: () => void;
};
type IState = {};
export default class Profile extends React.Component<IProps, IState> { export default function Profile() {
constructor(props: IProps) { const { isOpen, toggle, close } = useOpenable();
super(props);
this.state = { useEffect(() => {
isModalOpen: false, const onResize = (window: Window) => {
if (window.innerWidth < headerBreakpoint && isOpen) close();
}; };
}
public override render(): JSX.Element { const windowResizeSubscription = WindowStore.getInstance().onResize((window) => onResize(window));
return () => {
windowResizeSubscription();
};
}, [close, isOpen]);
return ( return (
<div className={classes["root"]}> <div className={classes["root"]}>
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.props.openProfileModal} /> <IconButton icon={<UserIcon />} onClick={toggle} />
{this.props.isModalOpen && <ProfileModal isOpen={this.props.isModalOpen} closeModal={this.props.closeProfileModal} />} <ProfileModal isOpen={isOpen} closeModal={close} />
</div> </div>
); );
}
} }

View File

@ -2,24 +2,20 @@
.root { .root {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
height: 83px; align-items: center;
background-color: var(--color-generic-white); flex-shrink: 0;
box-shadow: $shadow-nav;
padding: 0 48px; height: 75px;
padding: 0px var(--spacing-lg, 24px);
border-bottom: 1px solid var(--menu-border, #d7dce0);
background: var(--menu-background, #fff);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 4; z-index: 4;
@media (max-width: $screen-m) {
transition: transform 500ms ease-out;
&[data-open="closed"] {
transform: translateY(-85px);
}
}
.logo-container { .logo-container {
> a { > a {
cursor: pointer !important; cursor: pointer !important;
@ -31,45 +27,22 @@
} }
.right-section { .right-section {
.profile-section { display: flex;
display: inline-flex; align-items: center;
gap: var(--spacing-md);
}
@media (max-width: $screen-ls) { .desktop {
@media (max-width: $screen-m) {
display: none; display: none;
} }
} }
.notification-section { .mobile {
display: inline-flex;
> :first-child {
margin-right: 32px;
}
}
.help-section {
display: inline-flex;
cursor: pointer;
> :first-child {
margin-right: 32px;
}
@media (max-width: $screen-s) {
display: none;
}
}
}
.burger-menu {
display: none; display: none;
@media (max-width: $screen-ls) { @media (max-width: $screen-m) {
display: inline-flex; display: inline-flex;
.icon {
width: 24px;
height: 24px;
}
} }
} }
} }

View File

@ -1,183 +1,95 @@
import React from "react";
import classes from "./classes.module.scss";
import Image from "next/image";
import LogoIcon from "@Assets/logo_standard_neutral.svg"; import LogoIcon from "@Assets/logo_standard_neutral.svg";
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
import { InformationCircleIcon, LifebuoyIcon } from "@heroicons/react/24/outline";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
import IconButton from "../IconButton";
import Typography, { ETypo, ETypoColor } from "../Typography";
import BurgerMenu from "./BurgerMenu";
import classes from "./classes.module.scss";
import LogoCielNatureIcon from "./logo-ciel-notaires.jpeg";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import Notifications from "./Notifications"; import Notifications from "./Notifications";
import Profile from "./Profile"; import Profile from "./Profile";
import BurgerMenu from "./BurgerMenu";
import WindowStore from "@Front/Stores/WindowStore";
import Module from "@Front/Config/Module";
import Head from "next/head";
import { useRouter } from "next/router";
import LogoCielNatureIcon from "./logo-ciel-notaires.jpeg";
import LifeBuoy from "@Assets/Icons/life_buoy.svg";
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
import JwtService from "@Front/Services/JwtService/JwtService";
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
import Typography, { ETypo, ETypoColor } from "../Typography";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
enum EHeaderOpeningState {
OPEN = "open",
CLOSED = "closed",
IDLE = "idle",
}
type IProps = { type IProps = {
isUserConnected: boolean; isUserConnected: boolean;
}; };
type IPropsClass = IProps & { export default function Header(props: IProps) {
isOnCustomerLoginPage: boolean; const { isUserConnected } = props;
};
type IState = { const router = useRouter();
open: EHeaderOpeningState; const { pathname } = router;
isBurgerMenuOpen: boolean; const isOnCustomerLoginPage = Module.getInstance().get().modules.pages.CustomersLogin.props.path === pathname;
isNotificationMenuOpen: boolean;
isProfileMenuOpen: boolean;
cancelAt: Date | null;
};
class HeaderClass extends React.Component<IPropsClass, IState> { const [cancelAt, setCancelAt] = useState<Date | null>(null);
private onWindowResize = () => {};
private headerBreakpoint = 1300;
constructor(props: IPropsClass) { const loadSubscription = useCallback(async () => {
super(props); const jwt = JwtService.getInstance().decodeJwt();
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
this.state = { if (subscription[0]) {
open: EHeaderOpeningState.OPEN, const stripeSubscription = await Stripe.getInstance().getStripeSubscriptionByUid(subscription[0].stripe_subscription_id!);
isBurgerMenuOpen: false, if (stripeSubscription.cancel_at !== null) {
isNotificationMenuOpen: false, setCancelAt(new Date(stripeSubscription.cancel_at! * 1000));
isProfileMenuOpen: false,
cancelAt: null,
};
this.openBurgerMenu = this.openBurgerMenu.bind(this);
this.closeBurgerMenu = this.closeBurgerMenu.bind(this);
this.openNotificationMenu = this.openNotificationMenu.bind(this);
this.closeNotificationMenu = this.closeNotificationMenu.bind(this);
this.closeProfileMenu = this.closeProfileMenu.bind(this);
this.openProfileMenu = this.openProfileMenu.bind(this);
this.loadSubscription = this.loadSubscription.bind(this);
} }
}
}, []);
useEffect(() => {
loadSubscription();
}, [loadSubscription]);
public override render(): JSX.Element {
return ( return (
<> <>
<div className={classes["root"]} data-open={this.state.open}> <div className={classes["root"]}>
<Head> <Head>
<link rel="shortcut icon" href={"/favicon.svg"} /> <link rel="shortcut icon" href={"/favicon.svg"} />
</Head> </Head>
<div className={classes["logo-container"]}> <div className={classes["logo-container"]}>
<Link href={this.props.isUserConnected ? Module.getInstance().get().modules.pages.Folder.props.path : "#"}> <Link href={isUserConnected ? Module.getInstance().get().modules.pages.Folder.props.path : "#"}>
<Image src={LogoIcon} alt="logo" className={classes["logo"]} /> <Image src={LogoIcon} alt="logo" className={classes["logo"]} />
</Link> </Link>
</div> </div>
{this.props.isUserConnected && ( {isUserConnected && (
<> <>
<div className={classes["desktop"]}>
<Navigation /> <Navigation />
</div>
<div className={classes["right-section"]}> <div className={classes["right-section"]}>
<div className={classes["help-section"]}> <div className={classes["desktop"]}>
<Link href="https://tally.so/r/mBGaNY" target="blank"> <Link href="https://tally.so/r/mBGaNY" target="_blank">
<Image src={LifeBuoy} alt="help" /> <IconButton icon={<LifebuoyIcon />} />
</Link> </Link>
</div> </div>
<div className={classes["notification-section"]}> <Notifications />
<Notifications <div className={classes["desktop"]}>
isModalOpen={this.state.isNotificationMenuOpen} <Profile />
openNotificationModal={this.openNotificationMenu}
closeNotificationModal={this.closeNotificationMenu}
/>
</div> </div>
<div className={classes["profile-section"]}> <div className={classes["mobile"]}>
<Profile <BurgerMenu />
isModalOpen={this.state.isProfileMenuOpen}
closeProfileModal={this.closeProfileMenu}
openProfileModal={this.openProfileMenu}
/>
</div>
<div className={classes["burger-menu"]}>
<BurgerMenu
isModalOpen={this.state.isBurgerMenuOpen}
closeBurgerMenu={this.closeBurgerMenu}
openBurgerMenu={this.openBurgerMenu}
/>
</div> </div>
</div> </div>
</> </>
)} )}
{this.props.isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>} {isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>}
</div> </div>
{this.state.cancelAt && ( {cancelAt && (
<div className={classes["subscription-line"]}> <div className={classes["subscription-line"]}>
<InformationCircleIcon height="24" />; <InformationCircleIcon height="24" />
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}> <Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}>
Assurez vous de sauvegarder tout ce dont vous avez besoin avant la fin de votre abonnement le{" "} Assurez vous de sauvegarder tout ce dont vous avez besoin avant la fin de votre abonnement le{" "}
{this.state.cancelAt.toLocaleDateString()}. {cancelAt.toLocaleDateString()}.
</Typography> </Typography>
</div> </div>
)} )}
</> </>
); );
}
public override componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
this.loadSubscription();
}
public override componentWillUnmount() {
this.onWindowResize();
}
public async loadSubscription() {
const jwt = JwtService.getInstance().decodeJwt();
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (subscription[0]) {
const stripeSubscription = await Stripe.getInstance().getStripeSubscriptionByUid(subscription[0].stripe_subscription_id!);
if (stripeSubscription.cancel_at !== null)
this.setState({
...this.state,
cancelAt: new Date(stripeSubscription.cancel_at! * 1000),
});
}
}
private onResize(window: Window) {
if (window.innerWidth > this.headerBreakpoint && this.state.isBurgerMenuOpen) this.setState({ isBurgerMenuOpen: false });
if (window.innerWidth < this.headerBreakpoint && this.state.isProfileMenuOpen) this.setState({ isProfileMenuOpen: false });
}
private openBurgerMenu() {
this.setState({ isBurgerMenuOpen: true });
}
private closeBurgerMenu() {
this.setState({ isBurgerMenuOpen: false });
}
private openNotificationMenu() {
this.setState({ isNotificationMenuOpen: true });
}
private closeNotificationMenu() {
this.setState({ isNotificationMenuOpen: false });
}
private openProfileMenu() {
this.setState({ isProfileMenuOpen: true });
}
private closeProfileMenu() {
this.setState({ isProfileMenuOpen: false });
}
}
export default function Header(props: IProps) {
const router = useRouter();
const { pathname } = router;
let isActive = Module.getInstance().get().modules.pages.CustomersLogin.props.path === pathname;
return <HeaderClass {...props} isOnCustomerLoginPage={isActive} />;
} }

View File

@ -1,4 +1,4 @@
.menu-item-wrapper { .root {
width: 100%; width: 100%;
.menu-item { .menu-item {
display: flex; display: flex;

View File

@ -0,0 +1,65 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import React, { useCallback, useEffect } from "react";
import useHoverable from "@Front/Hooks/useHoverable";
import { IItem } from "..";
import classNames from "classnames";
type IProps = {
item: IItem;
closeMenuCb: () => void;
};
export default function MenuItem(props: IProps) {
const { item, closeMenuCb } = props;
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = React.useState(item.link === pathname);
const handleClickElement = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
closeMenuCb();
const link = e.currentTarget.getAttribute("data-link");
if (link) router.push(link);
if (item.onClick) item.onClick();
},
[closeMenuCb, item, router],
);
const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable();
const getColor = useCallback(() => {
if (isActive) return ETypoColor.CONTRAST_ACTIVED;
if (isHovered && item.color !== ETypoColor.ERROR_WEAK_CONTRAST) return ETypoColor.CONTRAST_HOVERED;
if (item.color) return item.color;
if (isHovered) return ETypoColor.CONTRAST_DEFAULT;
return ETypoColor.CONTRAST_DEFAULT;
}, [isActive, isHovered, item.color]);
useEffect(() => {
if (item.link === pathname) setIsActive(true);
if (item.routesActive) {
for (const routeActive of item.routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) setIsActive(true);
}
}
}, [isActive, item.link, item.routesActive, pathname]);
return (
<div
className={classNames(classes["root"], isActive && classes["active"])}
onClick={handleClickElement}
data-link={item.link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className={classes["menu-item"]}>
{React.cloneElement(item.icon, { color: `var(${getColor()})` })}
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
{item.text}
</Typography>
</div>
{item.hasSeparator && <div className={classes["separator"]} />}
</div>
);
}

View File

@ -5,25 +5,35 @@
.sub-menu { .sub-menu {
position: absolute; position: absolute;
top: 48px; top: 48px;
display: inline-flex; display: inline-flex;
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
border-radius: var(--menu-radius, 0px);
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
border-radius: var(--menu-radius, 0);
border: 1px solid var(--menu-border, #d7dce0); border: 1px solid var(--menu-border, #d7dce0);
background: var(--color-generic-white, #fff); background: var(--color-generic-white, #fff);
text-wrap: nowrap; text-wrap: nowrap;
/* shadow/sm */
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
z-index: 2; z-index: 3;
&[data-opening-side="left"] { &[data-opening-side="left"] {
left: auto; left: auto;
right: 0px; right: 0;
} }
&[data-opening-side="right"] { &[data-opening-side="right"] {
left: 0px; left: 0;
right: auto; right: auto;
} }
&[data-opening-side="center"] {
transform: translateX(-25%);
top: 90px;
}
} }
} }

View File

@ -0,0 +1,91 @@
import { IAppRule } from "@Front/Api/Entities/rule";
import { ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import useHoverable from "@Front/Hooks/useHoverable";
import useOpenable from "@Front/Hooks/useOpenable";
import React, { useEffect, useRef } from "react";
import classes from "./classes.module.scss";
import MenuItem from "./MenuItem";
type IItemBase = {
icon: JSX.Element;
text: string;
hasSeparator?: boolean;
color?: ETypoColor;
};
type IItemWithLink = IItemBase & {
link: string;
rules?: IAppRule[];
routesActive?: string[];
onClick?: never;
};
type IItemWithOnClick = IItemBase & {
onClick: () => void;
link?: never;
rules?: never;
routesActive?: never;
};
export type IItem = IItemWithLink | IItemWithOnClick;
type IProps = {
children: React.ReactNode;
items: IItem[];
openingSide?: "left" | "right" | "center";
openOnHover?: boolean;
};
export default function Menu(props: IProps) {
const { openingSide = "left", items, children, openOnHover } = props;
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(100);
const { isOpen, toggle, close } = useOpenable();
const subMenuRef = useRef<HTMLDivElement>(null);
const iconRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
subMenuRef.current &&
!subMenuRef.current.contains(e.target as Node) &&
iconRef.current &&
!iconRef.current.contains(e.target as Node)
) {
close();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [close]);
return (
<Rules mode={RulesMode.OPTIONAL} rules={items.flatMap((item) => item?.rules ?? [])}>
<div
className={classes["root"]}
onClick={toggle}
onMouseEnter={openOnHover ? handleMouseEnter : undefined}
onMouseLeave={openOnHover ? handleMouseLeave : undefined}>
<div ref={iconRef} className={classes["main"]}>
{children}
</div>
{((!openOnHover && isOpen) || (openOnHover && isHovered)) && (
<div className={classes["sub-menu"]} ref={subMenuRef} data-opening-side={openingSide}>
{items.map((item, index) => {
return (
<Rules mode={RulesMode.NECESSARY} rules={item.rules ?? []} key={item.link}>
<MenuItem item={item} key={index} closeMenuCb={close} />
</Rules>
);
})}
</div>
)}
</div>
</Rules>
);
}

View File

@ -153,6 +153,7 @@ export enum ETypoColor {
CONTRAST_DEFAULT = "--contrast-default", CONTRAST_DEFAULT = "--contrast-default",
CONTRAST_HOVERED = "--contrast-hovered", CONTRAST_HOVERED = "--contrast-hovered",
ERROR_WEAK_CONTRAST = "--error-weak-contrast", ERROR_WEAK_CONTRAST = "--error-weak-contrast",
CONTRAST_ACTIVED = "--contrast-actived",
NAVIGATION_BUTTON_CONTRAST_DEFAULT = "--navigation-button-contrast-default", NAVIGATION_BUTTON_CONTRAST_DEFAULT = "--navigation-button-contrast-default",
NAVIGATION_BUTTON_CONTRAST_ACTIVE = "--navigation-button-contrast-active", NAVIGATION_BUTTON_CONTRAST_ACTIVE = "--navigation-button-contrast-active",
} }

View File

@ -1,50 +0,0 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import useHoverable from "@Front/Hooks/useHoverable";
import { ISubElement } from "..";
type IProps = {
element: ISubElement;
closeMenuCb: () => void;
};
export default function SubMenuItem(props: IProps) {
const { element, closeMenuCb } = props;
const router = useRouter();
const handleClickElement = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
closeMenuCb();
const link = e.currentTarget.getAttribute("data-link");
if (link) router.push(link);
if (element.onClick) element.onClick();
},
[closeMenuCb, element, router],
);
const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable();
// The element has a link or an onClick but not both, if it has a link, show it has a link, if it has an onClick, show it has an onClick
const getColor = useCallback(() => {
if (isHovered && element.color !== ETypoColor.ERROR_WEAK_CONTRAST) return ETypoColor.CONTRAST_HOVERED;
if (element.color) return element.color;
return ETypoColor.CONTRAST_DEFAULT;
}, [element.color, isHovered]);
return (
<div
className={classes["menu-item-wrapper"]}
onClick={handleClickElement}
data-link={element.link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className={classes["menu-item"]}>
{React.cloneElement(element.icon, { color: `var(${getColor()})` })}
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
{element.text}
</Typography>
</div>
{element.hasSeparator && <div className={classes["separator"]} />}
</div>
);
}

View File

@ -1,78 +0,0 @@
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import classes from "./classes.module.scss";
import { ETypoColor } from "@Front/Components/DesignSystem/Typography";
import React, { useEffect, useRef, useState } from "react";
import SubMenuItem from "./SubMenuItem";
type ISubElementBase = {
icon: JSX.Element;
text: string;
hasSeparator?: boolean;
color?: ETypoColor;
};
type ISubElementWithLink = ISubElementBase & {
link: string;
onClick?: never;
};
type ISubElementWithOnClick = ISubElementBase & {
onClick: () => void;
link?: never;
};
export type ISubElement = ISubElementWithLink | ISubElementWithOnClick;
type IProps = {
icon: React.ReactNode;
text?: string;
subElements: ISubElement[];
openingSide?: "left" | "right";
};
export default function ButtonWithSubMenu(props: IProps) {
const { openingSide = "left" } = props;
const [isSubMenuOpened, setIsSubMenuOpened] = useState(false);
const subMenuRef = useRef<HTMLDivElement>(null);
const iconRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
setIsSubMenuOpened((prev) => !prev);
};
const closeMenu = () => {
setIsSubMenuOpened(false);
};
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
subMenuRef.current &&
!subMenuRef.current.contains(e.target as Node) &&
iconRef.current &&
!iconRef.current.contains(e.target as Node)
) {
setIsSubMenuOpened(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className={classes["root"]}>
<div className={classes["main"]} onClick={handleClick} ref={iconRef}>
<IconButton icon={props.icon} variant={EIconButtonVariant.NEUTRAL} />
</div>
{isSubMenuOpened && (
<div className={classes["sub-menu"]} ref={subMenuRef} data-opening-side={openingSide}>
{props.subElements.map((element, index) => {
return <SubMenuItem element={element} key={index} closeMenuCb={closeMenu} />;
})}
</div>
)}
</div>
);
}

View File

@ -26,7 +26,7 @@ export default function Rules(props: IProps) {
if (props.mode === RulesMode.NECESSARY) { if (props.mode === RulesMode.NECESSARY) {
return props.rules.every((rule) => JwtService.getInstance().hasRule(rule.name, rule.action)); return props.rules.every((rule) => JwtService.getInstance().hasRule(rule.name, rule.action));
} }
return !!props.rules.find((rule) => JwtService.getInstance().hasRule(rule.name, rule.action)); return props.rules.length === 0 || !!props.rules.find((rule) => JwtService.getInstance().hasRule(rule.name, rule.action));
}, [props.mode, props.rules]); }, [props.mode, props.rules]);
useEffect(() => { useEffect(() => {

View File

@ -27,7 +27,7 @@ import {
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu"; import Menu from "@Front/Components/DesignSystem/Menu";
export default function DesignSystem() { export default function DesignSystem() {
const { isOpen, open, close } = useOpenable(); const { isOpen, open, close } = useOpenable();
@ -80,9 +80,8 @@ export default function DesignSystem() {
<div className={classes["root"]}> <div className={classes["root"]}>
<div className={classes["components"]}> <div className={classes["components"]}>
<Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography> <Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography>
<ButtonWithSubMenu <Menu
icon={<EllipsisHorizontalIcon />} items={[
subElements={[
{ {
icon: <UsersIcon />, icon: <UsersIcon />,
text: "Modifier les collaborateurs", text: "Modifier les collaborateurs",
@ -101,8 +100,9 @@ export default function DesignSystem() {
link: "/", link: "/",
color: ETypoColor.ERROR_WEAK_CONTRAST, color: ETypoColor.ERROR_WEAK_CONTRAST,
}, },
]} ]}>
/> <IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Menu>
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography> <Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography> <Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} /> <NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />

View File

@ -1,17 +1,18 @@
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Menu from "@Front/Components/DesignSystem/Menu";
import Modal from "@Front/Components/DesignSystem/Modal";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module";
import useOpenable from "@Front/Hooks/useOpenable"; import useOpenable from "@Front/Hooks/useOpenable";
import { PencilSquareIcon, TrashIcon, UsersIcon } from "@heroicons/react/24/outline"; import { PencilSquareIcon, TrashIcon, UsersIcon } from "@heroicons/react/24/outline";
import { Note } from "le-coffre-resources/dist/Customer";
import { useCallback } from "react";
import { ICustomer } from ".."; import { ICustomer } from "..";
import { AnchorStatus } from "../.."; import { AnchorStatus } from "../..";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import DeleteCustomerModal from "./DeleteCustomerModal"; import DeleteCustomerModal from "./DeleteCustomerModal";
import Module from "@Front/Config/Module";
import { useCallback } from "react";
import { Note } from "le-coffre-resources/dist/Customer";
import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu";
import Modal from "@Front/Components/DesignSystem/Modal";
type IProps = { type IProps = {
customer: ICustomer; customer: ICustomer;
@ -56,10 +57,9 @@ export default function ClientBox(props: IProps) {
{customer.contact?.first_name} {customer.contact?.last_name} {customer.contact?.first_name} {customer.contact?.last_name}
</Typography> </Typography>
{anchorStatus === AnchorStatus.NOT_ANCHORED && ( {anchorStatus === AnchorStatus.NOT_ANCHORED && (
<ButtonWithSubMenu <Menu
icon={<PencilSquareIcon />}
openingSide="right" openingSide="right"
subElements={[ items={[
{ {
icon: <UsersIcon />, icon: <UsersIcon />,
text: "Modifier les informations", text: "Modifier les informations",
@ -74,12 +74,9 @@ export default function ClientBox(props: IProps) {
text: "Modifier la note", text: "Modifier la note",
link: createOrUpdateNotePath, link: createOrUpdateNotePath,
}, },
]} ]}>
/> <IconButton icon={<PencilSquareIcon />} variant={EIconButtonVariant.NEUTRAL} />
// <Link </Menu>
// href={}>
// <IconButton variant={EIconButtonVariant.NEUTRAL} />
// </Link>
)} )}
</div> </div>
<div> <div>

View File

@ -1,15 +1,16 @@
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Menu, { IItem } from "@Front/Components/DesignSystem/Menu";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline"; import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
import { OfficeFolder } from "le-coffre-resources/dist/Notary"; import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import classes from "./classes.module.scss";
import { AnchorStatus } from "..";
import ButtonWithSubMenu, { ISubElement } from "@Front/Components/Elements/ButtonWithSubMenu";
import { useCallback } from "react"; import { useCallback } from "react";
import { AnchorStatus } from "..";
import classes from "./classes.module.scss";
type IProps = { type IProps = {
folder: OfficeFolder | null; folder: OfficeFolder | null;
progress: number; progress: number;
@ -22,7 +23,7 @@ export default function InformationSection(props: IProps) {
const { folder, progress, onArchive, anchorStatus, isArchived } = props; const { folder, progress, onArchive, anchorStatus, isArchived } = props;
const getSubMenuElement = useCallback(() => { const getSubMenuElement = useCallback(() => {
let elements: ISubElement[] = []; let elements: IItem[] = [];
// Creating the three elements and adding them conditionnally // Creating the three elements and adding them conditionnally
const modifyCollaboratorsElement = { const modifyCollaboratorsElement = {
@ -86,7 +87,9 @@ export default function InformationSection(props: IProps) {
<div className={classes["progress-container"]}> <div className={classes["progress-container"]}>
<CircleProgress percentage={progress} /> <CircleProgress percentage={progress} />
<div className={classes["icon-container"]}> <div className={classes["icon-container"]}>
<ButtonWithSubMenu icon={<EllipsisHorizontalIcon />} subElements={getSubMenuElement()} /> <Menu items={getSubMenuElement()}>
<IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Menu>
</div> </div>
</div> </div>
<div className={classes["description-container"]}> <div className={classes["description-container"]}>